Part 3/3 A Full-Stack Expense Tracking App With Blazor, Web API & EF Core and SQL Server Express.
In Part 2 we built a working Frontend that shows the list of expenses and total expenses we also used services to make our code more modular. In Part 3 we would continue with hooking up the create, edit and delete actions in the frontend.
Fix Error Previous Error
From where we left off, there was an error "Unhandled exception rendering component: Object reference not set to an instance of an object".
To fix it:
In
BlazorExpenseTracker.Client/_Imports.razor
add an import statement for models.@using BlazorExpenseTracker.Shared.Models;
In
Index.razor
we need to initialize the variable for expenses and set its value afterawait ExpenseService.GetExpensesAsync();
Update the value used in the html when getting the list of expenses.
@page "/"
@inject IExpenseService ExpenseService
<PageTitle>Home</PageTitle>
<div style=" display: flex; flex-direction:column; justify-content:center">
<div style="display:flex; justify-content:end">
<a href="#" >
<svg style="width:50px; cursor:pointer " class="svg-icon" viewBox="0 0 20 20">
<path d="M14.613,10c0,0.23-0.188,0.419-0.419,0.419H10.42v3.774c0,0.23-0.189,0.42-0.42,0.42s-0.419-0.189-0.419-0.42v-3.774H5.806c-0.23,0-0.419-0.189-0.419-0.419s0.189-0.419,0.419-0.419h3.775V5.806c0-0.23,0.189-0.419,0.419-0.419s0.42,0.189,0.42,0.419v3.775h3.774C14.425,9.581,14.613,9.77,14.613,10 M17.969,10c0,4.401-3.567,7.969-7.969,7.969c-4.402,0-7.969-3.567-7.969-7.969c0-4.402,3.567-7.969,7.969-7.969C14.401,2.031,17.969,5.598,17.969,10 M17.13,10c0-3.932-3.198-7.13-7.13-7.13S2.87,6.068,2.87,10c0,3.933,3.198,7.13,7.13,7.13S17.13,13.933,17.13,10"></path>
</svg>
</a>
</div>
<div style="display:flex; flex-direction:column; text-align:center;">
<p style="color:gray">Total Expenses</p>
<h2 style="font-weight:600; margin-top:-10px"> $@Decimal.Round(@ExpenseService.TotalExpenses, 2)</h2>
</div>
@*Update the value used in the html when getting the list of expenses*@
@foreach(var expense in @expenses)
{
<div style=" border-bottom: 2px solid lightgray; margin-bottom:12px; padding-top:32px ">
<div style="display:flex; flex-direction: row; width:100%; cursor:pointer ; align-items:center; ">
<div style="height:64px; width: 100%;">
<p style="font-weight:600">@expense.Title</p>
<p style="color:gray">@expense.CreatedAt.ToString("dd MMM yyyy hh:mm tt")</p>
</div>
<div style="padding-right:12px">
$@Decimal.Round(@expense.Amount, 2)
</div>
<div style="padding:16px;">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16"> <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" /> </svg>
</div>
<div style=" padding:16px; background-color:antiquewhite; cursor:pointer ;">
<svg style="width:18px" class="svg-icon" viewBox="0 0 20 20">
<path d="M7.083,8.25H5.917v7h1.167V8.25z M18.75,3h-5.834V1.25c0-0.323-0.262-0.583-0.582-0.583H7.667
c-0.322,0-0.583,0.261-0.583,0.583V3H1.25C0.928,3,0.667,3.261,0.667,3.583c0,0.323,0.261,0.583,0.583,0.583h1.167v14
c0,0.644,0.522,1.166,1.167,1.166h12.833c0.645,0,1.168-0.522,1.168-1.166v-14h1.166c0.322,0,0.584-0.261,0.584-0.583
C19.334,3.261,19.072,3,18.75,3z M8.25,1.833h3.5V3h-3.5V1.833z M16.416,17.584c0,0.322-0.262,0.583-0.582,0.583H4.167
c-0.322,0-0.583-0.261-0.583-0.583V4.167h12.833V17.584z M14.084,8.25h-1.168v7h1.168V8.25z M10.583,7.083H9.417v8.167h1.167V7.083
z"></path>
</svg>
</div>
</div>
</div>
}
</div>
@code {
// Initialize the variable for expenses.
List<ExpenseModel> expenses = new List<ExpenseModel>();
protected override async Task OnInitializedAsync()
{
await ExpenseService.GetExpensesAsync();
expenses = ExpenseService.Expenses;
}
}
Add The Ability To View And Edit An Expense.
Server Project
Add GetExpenseDetailsAsync
In BlazorExpenseTracker.Server/Services/ExpenseService/IExpenseService.cs
Add Task<ExpenseModel> GetExpenseDetailsAsync(int id);
using BlazorExpenseTracker.Shared.Models;
namespace BlazorExpenseTracker.Server.Services.ExpenseService
{
public interface IExpenseService
{
Task<List<ExpenseModel>> GetExpensesAsync();
Task<ExpenseModel> CreateExpensesAsync(ExpenseModel expense);
Task<ExpenseModel> EditExpenseAsync(ExpenseModel expense, int id);
Task RemoveExpense(int id);
Task<ExpenseModel> GetExpenseDetailsAsync(int id);
}
}
In BlazorExpenseTracker.Server/Services/ExpenseService/ExpenseService.cs
Implement the new interface member.
using BlazorExpenseTracker.Server.Data;
using BlazorExpenseTracker.Shared.Models;
using Microsoft.EntityFrameworkCore;
namespace BlazorExpenseTracker.Server.Services.ExpenseService
{
public class ExpenseService : IExpenseService
{
private readonly DataContext _context;
public ExpenseService(DataContext context)
{
_context = context;
}
public async Task<ExpenseModel> CreateExpensesAsync(ExpenseModel expense)
{
var response = await _context.Expenses.AddAsync(expense);
await _context.SaveChangesAsync();
return response.Entity;
}
public async Task<ExpenseModel> EditExpenseAsync(ExpenseModel expense, int id)
{
ExpenseModel response = null;
var DbExpense = await _context.Expenses.FirstOrDefaultAsync(e => e.Id == id);
if (DbExpense != null)
{
DbExpense.Amount = expense.Amount;
DbExpense.Title = expense.Title;
DbExpense.Description = expense.Description;
DbExpense.CreatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
response = DbExpense;
}
return response;
}
public async Task<List<ExpenseModel>> GetExpensesAsync()
{
var response = await _context.Expenses.ToListAsync();
return response;
}
public async Task RemoveExpense(int id)
{
var DbExpense = await _context.Expenses.FirstOrDefaultAsync(e => e.Id == id);
if (DbExpense != null)
{
_context.Expenses.Remove(DbExpense);
}
await _context.SaveChangesAsync();
}
public async Task<ExpenseModel> GetExpenseDetailsAsync(int id)
{
var response = await _context.Expenses.FirstOrDefaultAsync(e => e.Id == id);
return response;
}
}
}
In ExpensesController.cs
Add GetExpenseDetailsAsync(int id)
.
using BlazorExpenseTracker.Server.Data;
using BlazorExpenseTracker.Server.Services.ExpenseService;
using BlazorExpenseTracker.Shared.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace BlazorExpenseTracker.Server.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ExpensesController : ControllerBase
{
private readonly IExpenseService _expenseService;
public ExpensesController(IExpenseService expenseService)
{
_expenseService = expenseService;
}
[HttpGet]
public async Task<ActionResult<List<ExpenseModel>>> GetAllExpensesAsync()
{
List<ExpenseModel> response = await _expenseService.GetExpensesAsync();
return Ok(response);
}
[HttpPost]
public async Task<ActionResult<ExpenseModel>> CreateExpenseAsync(ExpenseModel expense)
{
ExpenseModel response = await _expenseService.CreateExpensesAsync(expense);
return Ok(response);
}
[HttpPut]
[Route("{id}")]
public async Task<ActionResult<ExpenseModel>> EditExpenseAsync(ExpenseModel expense, int id)
{
ExpenseModel resoponse = await _expenseService.EditExpenseAsync(expense, id);
return Ok(resoponse);
}
[HttpDelete]
[Route("{id}")]
public async Task RemoveExpense(int id)
{
await _expenseService.RemoveExpense(id);
}
// Add GetExpenseDetailsAsync(int id)
[HttpGet]
[Route("{id}")]
public async Task<ActionResult<ExpenseModel>> GetExpenseDetailsAsync(int id)
{
var response = await _expenseService.GetExpenseDetailsAsync(id);
return Ok(response);
}
}
}
NOTE: Use Swagger to test the endpoint.
Client Project
Add GetExpenseDetailsAsync
- In
BlazorExpenseTracker.Client/Services/ExpenseService/IExpenseService.cs
AddTask<ExpenseModel> GetExpenseDetailsAsync(int id);
using BlazorExpenseTracker.Shared.Models;
namespace BlazorExpenseTracker.Client.Services.ExpenseService
{
public interface IExpenseService
{
List<ExpenseModel> Expenses { get; set; }
decimal TotalExpenses { get; set; }
Task<List<ExpenseModel>> GetExpensesAsync();
Task<ExpenseModel> CreateExpenseAsync(ExpenseModel expense);
Task<ExpenseModel> EditExpenseAsync(ExpenseModel expense, int id);
Task RemoveExpense(int id);
Task<ExpenseModel> GetExpenseDetailsAsync(int id);
}
}
In BlazorExpenseTracker.Client/Services/ExpenseService/ExpenseService.cs
Implement the new interface member.
using BlazorExpenseTracker.Shared.Models;
using System.Net.Http.Json;
namespace BlazorExpenseTracker.Client.Services.ExpenseService
{
public class ExpenseService : IExpenseService
{
private readonly HttpClient _http;
public ExpenseService(HttpClient http)
{
_http = http;
}
public List<ExpenseModel> Expenses { get; set ; }
public decimal TotalExpenses { get; set; }
public async Task<ExpenseModel> CreateExpenseAsync(ExpenseModel expense)
{
var response = await _http.PostAsJsonAsync<ExpenseModel>("/api/Expenses", expense);
return await response.Content.ReadFromJsonAsync<ExpenseModel>();
}
public async Task<ExpenseModel> EditExpenseAsync(ExpenseModel expense, int id)
{
var response = await _http.PutAsJsonAsync<ExpenseModel>($"/api/Expenses/{id}", expense);
return await response.Content.ReadFromJsonAsync<ExpenseModel>();
}
public async Task<List<ExpenseModel>> GetExpensesAsync()
{
var response = await _http.GetFromJsonAsync<List<ExpenseModel>>("/api/Expenses");
if (response != null)
{
Expenses = response;
CalculateTotalExpenses();
}
return Expenses;
}
public Task RemoveExpense(int id)
{
throw new NotImplementedException();
}
private void CalculateTotalExpenses()
{
TotalExpenses= 0;
foreach ( var expense in Expenses)
{
TotalExpenses += expense.Amount;
}
}
public async Task<ExpenseModel> GetExpenseDetailsAsync(int id)
{
var response = await _http.GetFromJsonAsync<ExpenseModel>($"/api/Expenses/{id}");
return response;
}
}
}
Add Page ExpenseForm
In BlazorExpenseTracker.Client/Pages
add ExpenseForm.razor
and add the code below.
Right-Click
Pages Folder
->Add
->Razor Component...
.Name the component
ExpenseForm.razor
.
@page "/expense/{id:int}"
@page "/expense"
@inject IExpenseService ExpenseService
<h3>ExpenseForm</h3>
<EditForm Model="@Expense" OnSubmit="HandleSubmit">
<div style="display:flex; flex-direction:column; max-width: 500px">
<label for="title">Title</label>
<InputText id="title" @bind-Value="Expense.Title" />
<label for="description" style="margin-top:16px">Description </label>
<InputText id="description" @bind-Value="Expense.Description" />
<label for="amount" style="margin-top:16px">Amount</label>
<InputNumber id="amount" @bind-Value="Expense.Amount" />
<button type="submit" style="margin-top:32px; width:150px;">Submit</button>
</div>
</EditForm>
@code {
[Parameter] public int Id { get; set; }
public ExpenseModel Expense { get; set; } = new ExpenseModel();
protected override async Task OnParametersSetAsync()
{
Expense = await ExpenseService.GetExpenseDetailsAsync(Id);
}
private async void HandleSubmit()
{
await ExpenseService.EditExpenseAsync(Expense, Expense.Id);
StateHasChanged();
}
}
Add OnClick Event Listener To Each Item In the List of Expenses
In
Index.razor
, add@onclick="() => OpenEditForm(
expense.Id
)"
to the edit icon.Inject NavigationManager and create the
OpenEditForm(int id)
method which would navigate to the specific URL when clicked.Add
cursor: pointer
tostyle
@page "/"
@inject IExpenseService ExpenseService
@inject NavigationManager NavigationManager
<PageTitle>Home</PageTitle>
<div style=" display: flex; flex-direction:column; justify-content:center">
<div style="display:flex; justify-content:end">
<a href="#" >
<svg style="width:50px; cursor:pointer " class="svg-icon" viewBox="0 0 20 20">
<path d="M14.613,10c0,0.23-0.188,0.419-0.419,0.419H10.42v3.774c0,0.23-0.189,0.42-0.42,0.42s-0.419-0.189-0.419-0.42v-3.774H5.806c-0.23,0-0.419-0.189-0.419-0.419s0.189-0.419,0.419-0.419h3.775V5.806c0-0.23,0.189-0.419,0.419-0.419s0.42,0.189,0.42,0.419v3.775h3.774C14.425,9.581,14.613,9.77,14.613,10 M17.969,10c0,4.401-3.567,7.969-7.969,7.969c-4.402,0-7.969-3.567-7.969-7.969c0-4.402,3.567-7.969,7.969-7.969C14.401,2.031,17.969,5.598,17.969,10 M17.13,10c0-3.932-3.198-7.13-7.13-7.13S2.87,6.068,2.87,10c0,3.933,3.198,7.13,7.13,7.13S17.13,13.933,17.13,10"></path>
</svg>
</a>
</div>
<div style="display:flex; flex-direction:column; text-align:center;">
<p style="color:gray">Total Expenses</p>
<h2 style="font-weight:600; margin-top:-10px"> $@Decimal.Round(@ExpenseService.TotalExpenses, 2)</h2>
</div>
@*Update the value used in the html when getting the list of expenses*@
@foreach(var expense in @expenses)
{
<div style=" border-bottom: 2px solid lightgray; margin-bottom:12px; padding-top:32px ">
<div style="display:flex; flex-direction: row; width:100%; cursor:pointer ; align-items:center; ">
<div style="height:64px; width: 100%;">
<p style="font-weight:600">@expense.Title</p>
<p style="color:gray">@expense.CreatedAt.ToString("dd MMM yyyy hh:mm tt")</p>
</div>
<div style="padding-right:12px">
$@Decimal.Round(@expense.Amount, 2)
</div>
<div @onclick="() => OpenEditForm(expense.Id)" style="padding:16px; cursor: pointer">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16"> <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" /> </svg>
</div>
<div style=" padding:16px; background-color:antiquewhite; cursor:pointer ;">
<svg style="width:18px" class="svg-icon" viewBox="0 0 20 20">
<path d="M7.083,8.25H5.917v7h1.167V8.25z M18.75,3h-5.834V1.25c0-0.323-0.262-0.583-0.582-0.583H7.667
c-0.322,0-0.583,0.261-0.583,0.583V3H1.25C0.928,3,0.667,3.261,0.667,3.583c0,0.323,0.261,0.583,0.583,0.583h1.167v14
c0,0.644,0.522,1.166,1.167,1.166h12.833c0.645,0,1.168-0.522,1.168-1.166v-14h1.166c0.322,0,0.584-0.261,0.584-0.583
C19.334,3.261,19.072,3,18.75,3z M8.25,1.833h3.5V3h-3.5V1.833z M16.416,17.584c0,0.322-0.262,0.583-0.582,0.583H4.167
c-0.322,0-0.583-0.261-0.583-0.583V4.167h12.833V17.584z M14.084,8.25h-1.168v7h1.168V8.25z M10.583,7.083H9.417v8.167h1.167V7.083
z"></path>
</svg>
</div>
</div>
</div>
}
</div>
@code {
// Initialize the variable for expenses.
List<ExpenseModel> expenses = new List<ExpenseModel>();
protected override async Task OnInitializedAsync()
{
await ExpenseService.GetExpensesAsync();
expenses = ExpenseService.Expenses;
}
private async void OpenEditForm(int id)
{
NavigationManager.NavigateTo($"/expense/{id}");
}
}
Add The Ability To Create A New Transaction
In Index.razor
change href = "#"
to href = "/expense"
@page "/"
@inject IExpenseService ExpenseService
@inject NavigationManager NavigationManager
<PageTitle>Home</PageTitle>
<div style=" display: flex; flex-direction:column; justify-content:center">
<div style="display:flex; justify-content:end">
@*change href = "#" to href = "/expense"*@
<a href="/expense" >
<svg style="width:50px; cursor:pointer " class="svg-icon" viewBox="0 0 20 20">
<path d="M14.613,10c0,0.23-0.188,0.419-0.419,0.419H10.42v3.774c0,0.23-0.189,0.42-0.42,0.42s-0.419-0.189-0.419-0.42v-3.774H5.806c-0.23,0-0.419-0.189-0.419-0.419s0.189-0.419,0.419-0.419h3.775V5.806c0-0.23,0.189-0.419,0.419-0.419s0.42,0.189,0.42,0.419v3.775h3.774C14.425,9.581,14.613,9.77,14.613,10 M17.969,10c0,4.401-3.567,7.969-7.969,7.969c-4.402,0-7.969-3.567-7.969-7.969c0-4.402,3.567-7.969,7.969-7.969C14.401,2.031,17.969,5.598,17.969,10 M17.13,10c0-3.932-3.198-7.13-7.13-7.13S2.87,6.068,2.87,10c0,3.933,3.198,7.13,7.13,7.13S17.13,13.933,17.13,10"></path>
</svg>
</a>
</div>
<div style="display:flex; flex-direction:column; text-align:center;">
<p style="color:gray">Total Expenses</p>
<h2 style="font-weight:600; margin-top:-10px"> $@Decimal.Round(@ExpenseService.TotalExpenses, 2)</h2>
</div>
@*Update the value used in the html when getting the list of expenses*@
@foreach(var expense in @expenses)
{
<div style=" border-bottom: 2px solid lightgray; margin-bottom:12px; padding-top:32px ">
<div style="display:flex; flex-direction: row; width:100%; cursor:pointer ; align-items:center; ">
<div style="height:64px; width: 100%;">
<p style="font-weight:600">@expense.Title</p>
<p style="color:gray">@expense.CreatedAt.ToString("dd MMM yyyy hh:mm tt")</p>
</div>
<div style="padding-right:12px">
$@Decimal.Round(@expense.Amount, 2)
</div>
<div @onclick="() => OpenEditForm(expense.Id)" style="padding:16px; cursor: pointer">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16"> <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" /> </svg>
</div>
<div style=" padding:16px; background-color:antiquewhite; cursor:pointer ;">
<svg style="width:18px" class="svg-icon" viewBox="0 0 20 20">
<path d="M7.083,8.25H5.917v7h1.167V8.25z M18.75,3h-5.834V1.25c0-0.323-0.262-0.583-0.582-0.583H7.667
c-0.322,0-0.583,0.261-0.583,0.583V3H1.25C0.928,3,0.667,3.261,0.667,3.583c0,0.323,0.261,0.583,0.583,0.583h1.167v14
c0,0.644,0.522,1.166,1.167,1.166h12.833c0.645,0,1.168-0.522,1.168-1.166v-14h1.166c0.322,0,0.584-0.261,0.584-0.583
C19.334,3.261,19.072,3,18.75,3z M8.25,1.833h3.5V3h-3.5V1.833z M16.416,17.584c0,0.322-0.262,0.583-0.582,0.583H4.167
c-0.322,0-0.583-0.261-0.583-0.583V4.167h12.833V17.584z M14.084,8.25h-1.168v7h1.168V8.25z M10.583,7.083H9.417v8.167h1.167V7.083
z"></path>
</svg>
</div>
</div>
</div>
}
</div>
@code {
// Initialize the variable for expenses.
List<ExpenseModel> expenses = new List<ExpenseModel>();
protected override async Task OnInitializedAsync()
{
await ExpenseService.GetExpensesAsync();
expenses = ExpenseService.Expenses;
}
private async void OpenEditForm(int id)
{
NavigationManager.NavigateTo($"/expense/{id}");
}
}
In ExpenseForm.razor
inject NavigationManager.
Check the parameter Id's value before getting expense details and also while submitting the form, to determine whether to edit or create an expense.
Navigate to "/"
@page "/expense/{id:int}"
@page "/expense"
@inject IExpenseService ExpenseService
@inject NavigationManager NavigationManager
<h3>ExpenseForm</h3>
<EditForm Model="@Expense" OnSubmit="HandleSubmit">
<div style="display:flex; flex-direction:column; max-width: 500px">
<label for="title">Title</label>
<InputText id="title" @bind-Value="Expense.Title" />
<label for="description" style="margin-top:16px">Description </label>
<InputText id="description" @bind-Value="Expense.Description" />
<label for="amount" style="margin-top:16px">Amount</label>
<InputNumber id="amount" @bind-Value="Expense.Amount" />
<button type="submit" style="margin-top:32px; width:150px;">Submit</button>
</div>
</EditForm>
@code {
[Parameter] public int Id { get; set; }
public ExpenseModel Expense { get; set; } = new ExpenseModel();
protected override async Task OnParametersSetAsync()
{
if (Id != 0)
{
Expense = await ExpenseService.GetExpenseDetailsAsync(Id);
}
}
private async void HandleSubmit()
{
if (Id == 0)
{
await ExpenseService.CreateExpenseAsync(Expense);
}
else
{
await ExpenseService.EditExpenseAsync(Expense, Expense.Id);
}
NavigationManager.NavigateTo("/");
StateHasChanged();
}
}
Add The Ability To Delete A Transaction
In BlazorExpenseTracker.Client.Services.ExpenseService/ExpenseService.cs
- Add async and Implement RemoveExpense
using BlazorExpenseTracker.Shared.Models;
using System.Net.Http.Json;
namespace BlazorExpenseTracker.Client.Services.ExpenseService
{
public class ExpenseService : IExpenseService
{
private readonly HttpClient _http;
public ExpenseService(HttpClient http)
{
_http = http;
}
public List<ExpenseModel> Expenses { get; set ; }
public decimal TotalExpenses { get; set; }
public async Task<ExpenseModel> CreateExpenseAsync(ExpenseModel expense)
{
var response = await _http.PostAsJsonAsync<ExpenseModel>("/api/Expenses", expense);
return await response.Content.ReadFromJsonAsync<ExpenseModel>();
}
public async Task<ExpenseModel> EditExpenseAsync(ExpenseModel expense, int id)
{
var response = await _http.PutAsJsonAsync<ExpenseModel>($"/api/Expenses/{id}", expense);
return await response.Content.ReadFromJsonAsync<ExpenseModel>();
}
public async Task<List<ExpenseModel>> GetExpensesAsync()
{
var response = await _http.GetFromJsonAsync<List<ExpenseModel>>("/api/Expenses");
if (response != null)
{
Expenses = response;
CalculateTotalExpenses();
}
return Expenses;
}
public async Task RemoveExpense(int id)
{
await _http.DeleteAsync($"/api/Expenses/{id}");
}
private void CalculateTotalExpenses()
{
TotalExpenses= 0;
foreach ( var expense in Expenses)
{
TotalExpenses += expense.Amount;
}
}
public async Task<ExpenseModel> GetExpenseDetailsAsync(int id)
{
var response = await _http.GetFromJsonAsync<ExpenseModel>($"/api/Expenses/{id}");
return response;
}
}
}
In Index.razor
Add
@onclick="() => DeleteExpense(
expense.Id
)"
Add the method in the code section.
@page "/"
@inject IExpenseService ExpenseService
@inject NavigationManager NavigationManager
<PageTitle>Home</PageTitle>
<div style=" display: flex; flex-direction:column; justify-content:center">
<div style="display:flex; justify-content:end">
@*change href = "#" to href = "/expense"*@
<a href="/expense" >
<svg style="width:50px; cursor:pointer " class="svg-icon" viewBox="0 0 20 20">
<path d="M14.613,10c0,0.23-0.188,0.419-0.419,0.419H10.42v3.774c0,0.23-0.189,0.42-0.42,0.42s-0.419-0.189-0.419-0.42v-3.774H5.806c-0.23,0-0.419-0.189-0.419-0.419s0.189-0.419,0.419-0.419h3.775V5.806c0-0.23,0.189-0.419,0.419-0.419s0.42,0.189,0.42,0.419v3.775h3.774C14.425,9.581,14.613,9.77,14.613,10 M17.969,10c0,4.401-3.567,7.969-7.969,7.969c-4.402,0-7.969-3.567-7.969-7.969c0-4.402,3.567-7.969,7.969-7.969C14.401,2.031,17.969,5.598,17.969,10 M17.13,10c0-3.932-3.198-7.13-7.13-7.13S2.87,6.068,2.87,10c0,3.933,3.198,7.13,7.13,7.13S17.13,13.933,17.13,10"></path>
</svg>
</a>
</div>
<div style="display:flex; flex-direction:column; text-align:center;">
<p style="color:gray">Total Expenses</p>
<h2 style="font-weight:600; margin-top:-10px"> $@Decimal.Round(@ExpenseService.TotalExpenses, 2)</h2>
</div>
@*Update the value used in the html when getting the list of expenses*@
@foreach(var expense in @expenses)
{
<div style=" border-bottom: 2px solid lightgray; margin-bottom:12px; padding-top:32px ">
<div style="display:flex; flex-direction: row; width:100%; cursor:pointer ; align-items:center; ">
<div style="height:64px; width: 100%;">
<p style="font-weight:600">@expense.Title</p>
<p style="color:gray">@expense.CreatedAt.ToString("dd MMM yyyy hh:mm tt")</p>
</div>
<div style="padding-right:12px">
$@Decimal.Round(@expense.Amount, 2)
</div>
<div @onclick="() => OpenEditForm(expense.Id)" style="padding:16px; cursor: pointer">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16"> <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" /> </svg>
</div>
<div @onclick="() => DeleteExpense(expense.Id)" style=" padding:16px; background-color:antiquewhite; cursor:pointer ;">
<svg style="width:18px" class="svg-icon" viewBox="0 0 20 20">
<path d="M7.083,8.25H5.917v7h1.167V8.25z M18.75,3h-5.834V1.25c0-0.323-0.262-0.583-0.582-0.583H7.667
c-0.322,0-0.583,0.261-0.583,0.583V3H1.25C0.928,3,0.667,3.261,0.667,3.583c0,0.323,0.261,0.583,0.583,0.583h1.167v14
c0,0.644,0.522,1.166,1.167,1.166h12.833c0.645,0,1.168-0.522,1.168-1.166v-14h1.166c0.322,0,0.584-0.261,0.584-0.583
C19.334,3.261,19.072,3,18.75,3z M8.25,1.833h3.5V3h-3.5V1.833z M16.416,17.584c0,0.322-0.262,0.583-0.582,0.583H4.167
c-0.322,0-0.583-0.261-0.583-0.583V4.167h12.833V17.584z M14.084,8.25h-1.168v7h1.168V8.25z M10.583,7.083H9.417v8.167h1.167V7.083
z"></path>
</svg>
</div>
</div>
</div>
}
</div>
@code {
// Initialize the variable for expenses.
List<ExpenseModel> expenses = new List<ExpenseModel>();
protected override async Task OnInitializedAsync()
{
await ExpenseService.GetExpensesAsync();
expenses = ExpenseService.Expenses;
}
private async void OpenEditForm(int id)
{
NavigationManager.NavigateTo($"/expense/{id}");
}
private async void DeleteExpense(int id)
{
await ExpenseService.RemoveExpense(id);
await ExpenseService.GetExpensesAsync();
StateHasChanged();
}
}
Use CreatedAt to order the list of Expenses
In BlazorExpenseTracker.Server/Services/ExpenseService/ExpenseService.cs
Fix CreatedAt by updating its value anytime a new expense is created.
Use time to order the list of Expenses.
using BlazorExpenseTracker.Server.Data;
using BlazorExpenseTracker.Shared.Models;
using Microsoft.EntityFrameworkCore;
namespace BlazorExpenseTracker.Server.Services.ExpenseService
{
public class ExpenseService : IExpenseService
{
private readonly DataContext _context;
public ExpenseService(DataContext context)
{
_context = context;
}
public async Task<ExpenseModel> CreateExpensesAsync(ExpenseModel expense)
{
// Fix CreatedAt by updating its value anytime a new expense is created.
expense.CreatedAt = DateTime.UtcNow;
var response = await _context.Expenses.AddAsync(expense);
await _context.SaveChangesAsync();
return response.Entity;
}
public async Task<ExpenseModel> EditExpenseAsync(ExpenseModel expense, int id)
{
ExpenseModel response = null;
var DbExpense = await _context.Expenses.FirstOrDefaultAsync(e => e.Id == id);
if (DbExpense != null)
{
DbExpense.Amount = expense.Amount;
DbExpense.Title = expense.Title;
DbExpense.Description = expense.Description;
DbExpense.CreatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
response = DbExpense;
}
return response;
}
public async Task<List<ExpenseModel>> GetExpensesAsync()
{
//Use time to order the list of Expenses
var response = await _context.Expenses.OrderByDescending(e => e.CreatedAt).ToListAsync();
return response;
}
public async Task RemoveExpense(int id)
{
var DbExpense = await _context.Expenses.FirstOrDefaultAsync(e => e.Id == id);
if (DbExpense != null)
{
_context.Expenses.Remove(DbExpense);
}
await _context.SaveChangesAsync();
}
public async Task<ExpenseModel> GetExpenseDetailsAsync(int id)
{
var response = await _context.Expenses.FirstOrDefaultAsync(e => e.Id == id);
return response;
}
}
}
Conclusion
This is a simple project which introduced different aspects of building a full-stack CRUD application using Blazor WASM, Web API , Entity Framework Core and SQL Server.
Subscribe to my newsletter
Read articles from Marvin Nii-Odai Botchway directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Marvin Nii-Odai Botchway
Marvin Nii-Odai Botchway
I am a web developer still learning but I love to share what I learn.