Part 3/3 A Full-Stack Expense Tracking App With Blazor, Web API & EF Core and SQL Server Express.

Part 3/3 A Full-Stack Expense Tracking App With Blazor, Web API & EF Core and SQL Server Express.

Finish Front-end

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 after await 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 Add Task<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 to style

@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.