App-Idea 3: Ultra Number Converter

Inspired by the number converters in the app-ideas project, I decided to create an uber number converter that allows users to pick from various number systems and convert to another one… instead of build more converters for octal and hexadecimal. Plus, this allows us to learn about build more complex UI in Blazor. In this project, we will work with some Blazor input components (EditForm, InputText, and InputSelect), rather than just straight HTML elements.

Here are the user stories that we will support in this project:

  • User should be able to enter one number in an input field
  • User should be able to pick the number system of the entered number
  • User should be able to pick the number system of the target result
  • User could see the results in a single output field containing the converted value on a button click
  • If a wrong symbol is entered, the user should see an error

We will add this page to the existing Blazor.AppIdeas.Converters app (rather than creating a separate full project). You can find the source for the Blazor.AppIdeas.Converters on GitHub. And the running sample of the Blazor app online.

We start by loading Blazor.AppIdeas.Converters solution into Visual Studio.

Model Classes

As with most of our projects, we will follow the MVVM design pattern for building UI and testable logic classes. First, this app also supports roman numeral conversions, so we will re-use the RomanNumeral model class from the App-Idea 2: Roman to Decimal article.

Then we define the NumberSystem enum in the Blazor.AppIdeas.Converters project and Models folder. This enum contains the various number systems our converter will support. We can extend the systems supported by adding more here.

namespace Blazor.AppIdeas.Converters.Models
{
    public enum NumberSystem
    {
        Binary,
        Octal,
        Decimal,
        Hexadecimal,
        Roman
    }
}

Then, we create the NumberConversionStrategy class in the Blazor.AppIdeas.Converters project and Models folder.

using System;
using System.Collections.Generic;

namespace Blazor.AppIdeas.Converters.Models
{
    public static class NumberConversionStrategy
    {
        private static readonly IDictionary<NumberSystem, int> _numberSytemBaseMapping =
            new Dictionary<NumberSystem, int>
            {
                { NumberSystem.Binary, 2 },
                { NumberSystem.Octal, 8 },
                { NumberSystem.Decimal, 10 },
                { NumberSystem.Hexadecimal, 16 },
            };

        private static readonly IDictionary<NumberSystem, string> _numberSystemErrorMessageMapping =
            new Dictionary<NumberSystem, string>
            {
                { NumberSystem.Binary, "Binary numbers only support digits: 0s and 1s." },
                { NumberSystem.Octal, "Octal numbers only support digits: 0-7." },
                { NumberSystem.Decimal, "Decimal numbers only support digits: 0-9." },
                { NumberSystem.Hexadecimal, "Hexadecimal numbers only support: digits 0-9 and characters A-F." },
                { NumberSystem.Roman, "Roman numerals only support the following characters: I, V, X, L, C, D, M." }
            };

        public static int ConvertFrom(string value, NumberSystem system)
        {
            return system switch
            {
                NumberSystem.Roman => new RomanNumeral(value).ToInt(),
                _ => Convert.ToInt32(value, _numberSytemBaseMapping[system]),
            };
        }

        public static string ConvertTo(int value, NumberSystem system)
        {
            return system switch
            {
                NumberSystem.Roman => RomanNumeral.FromDecimal(value).Value,
                _ => Convert.ToString(value, _numberSytemBaseMapping[system]),
            };
        }

        public static string GetNumberSystemErrorMessage(NumberSystem system) =>
            _numberSystemErrorMessageMapping[system];
    }
}

This class is an implementation of the Strategy design pattern. It provides different implementation of number converters based on the the number system specified. It hides the complexity of different conversions from its calling code.

  1. We define the _numberSytemBaseMapping member variable (lines #8-15) which maps the base for each number system. This is only used for Arabic numbers because they can use the same Convert system class.
  2. We define error message look-ups for all of the supported number systems (lines #17-25). We did this because we want to supply a specific error message for each number system (with their supported values). We could have simplified this by just providing a single generic error message.
  3. Because there is a matrix of conversion possibilities, we implemented the conversion in two distinct steps. This made testing the variations simpler.
    • The ConvertFrom method (lines #27-34) takes a text value and its number system, and converts it to an integer value in base-10. For roman numerals it uses that class, otherwise it uses the .NET Convert class.
    • The ConvertTo method (lines #36-43) then takes an base-10 integer value and the target number system, and converts it to a string of the specified number system. Roman numerals are a special case again, but the others use the .NET Convert.ToString method.
    • These methods use the condensed notation of switch expressions that was introduced in C#8.
  4. Finally, the GetNumberSystemErrorMessage method (lines #45-46) is a table lookup to get the appropriate error message for the specified number system.

NumberConverterViewModel

Next, we need to define the view model that is used for databinding, collecting all of the user inputs, responding to events, and handling error conditions.

Let’s create the NumberConverterViewModel in the Blazor.AppIdeas.Converters project and ViewModels folder.

using Blazor.AppIdeas.Converters.Models;
using System;

namespace Blazor.AppIdeas.Converters.ViewModels
{
    public class NumberConverterViewModel
    {
        public string EntryValue { get; set; }

        public NumberSystem EntryNumberSystem { get; set; } = NumberSystem.Binary;

        public string ResultValue { get; set; }

        public NumberSystem ResultNumberSystem { get; set; } = NumberSystem.Decimal;

        public string ErrorMessage { get; private set; }

        public bool HasError => !string.IsNullOrEmpty(ErrorMessage);

        public void Convert()
        {
            try
            {
                ErrorMessage = null;
                if (string.IsNullOrEmpty(EntryValue)) throw new FormatException();

                var valueAsInt = NumberConversionStrategy.ConvertFrom(EntryValue, EntryNumberSystem);
                ResultValue = NumberConversionStrategy.ConvertTo(valueAsInt, ResultNumberSystem);
            }
            catch
            {
                ErrorMessage = NumberConversionStrategy.GetNumberSystemErrorMessage(EntryNumberSystem);
            }
        }
    }
}
  1. We start with properties for the entered number and its number system (binary, octal, decimal, hexadecimal, and roman).
  2. Then, we have properties for the result value and its number system.
  3. And, an error message (if an error happened during the conversion).
  4. HasError is a derived property that is true only when an error message is set.
  5. Finally, the Convert method (lines #20-34):
    • Clears the error message (if any was there before).
    • Converts the EnteryValue to a base-10 integer.
    • Then, converts the integer to the selected target number system and saves it to the ResultValue.
    • Finally, if there was an exception thrown during the conversion, we set the ErrorMessage to a number system specific string.

This is all of the logic we need to build our conversion user interface.

Page Class and Helper

The Blazor page represents the view in the MVVM design pattern. So, we create the NumberConverter page in the Blazor.AppIdeas.Converters project and Pages folder.

@page "/number-converter"
@inject NumberConverterViewModel vm

<div class="col-lg-8 col-md-10 offset-lg-2 offset-md-1 mt-3 pb-3 container">
    <h3 class="mt-3">Ultra Number Converter</h3>
    <hr />
    <EditForm class="form-row" Model=@vm>
        <div class="form-group col-lg-6 col-md-12">
            <InputText id="entry-value" class="form-control"
                       placeholder="Enter number" @bind-Value=vm.EntryValue />
            <InputSelect id="entry-number-system" class="col-12" style="margin-top: 1px"
                         @bind-Value=vm.EntryNumberSystem>
                <option value="Binary">Binary</option>
                <option value="Octal">Octal</option>
                <option value="Decimal">Decimal</option>
                <option value="Hexadecimal">Hexadecimal</option>
                <option value="Roman">Roman</option>
            </InputSelect>
        </div>
        <div class="form-group col-lg-6 col-md-12">
            <InputText id="result-value" class="form-control" readonly
                       placeholder="Result..." @bind-Value=vm.ResultValue />
            <InputSelect id="result-number-system" class="col-12" style="margin-top: 1px"
                         @bind-Value=vm.ResultNumberSystem>
                @foreach (var enumItem in HtmlFormatHelper.GetEnumData<NumberSystem>())
                {
                    <option value="@enumItem.Item2">@enumItem.Item1</option>
                }
            </InputSelect>
        </div>
        @if (vm.HasError)
        {
        <div class="alert alert-danger col-12 ml-1">
            <strong>Error:</strong> @vm.ErrorMessage
        </div>
        }
    </EditForm>

    <div class="text-center">
        <input id="btn-convert" type="button" class="btn btn-outline-primary"
               value="Convert" @onclick="@vm.Convert" />
    </div>
</div>

@code {
}
  • The @page directive (line #1) defines the route to this page.
  • The @inject directive (line #2) retrieves the view model object from the dependency injection container.
  • We use basic HTML to define the control and the header.
  • The EditForm Blazor component (line #7) is used (rather than just <form>) to setup the form with binding to the view model using its Model attribute. EditForm must bind to a Model or it will throw an exception when rendered.
  • The InputText component wraps the <input> element. Its Value attribute property must also be bound to a property, and can only be used inside of an EditForm.
  • Then, the InputSelect component (lines #11-12) implements binding and updates with its Value attribute. It is also a templated component, so we can use specific types (like NumberSystem) as option values. This saves us having to write conversion logic from strings to other types.
  • Next, we create an <option> for each selectable number system (lines #13-17).
  • We then define another InputText and InputSelect for the result value and system following the same pattern.
  • For the second InputSelect we write code to add the options programmatically (lines #25-28) from the enum using HtmlFormatHelper.GetEnumData. We can use the @ directive to write code directly in the markup section of a razor page to loop through elements. We added select options in two different ways on this page to show both options (typically we would just use one approach).
  • Then, we used the HasError property to optionally render the error section. When there is no error, none of the HMTL is output. For performance reasons, it’s important to render the minimal HTML rather than hiding and showing segments of HTML.
  • Finally, we define a ‘Convert’ button that binds its onclick event to the view model’s Convert method.

This page shows the difference between using straight HTML and Blazor components to define similar UIs (in the other converter projects). In the end, the Blazor components render down to HTML elements but provide additional capabilities.

As shown in the page above, we use a helper class to convert the NumberSystem enum into a list of names and values. Let’s create the HtmlFormatHelper class in the Blazor.AppIdeas.Converters project and Pages folder.

using System;
using System.Collections.Generic;

namespace Blazor.AppIdeas.Converters.Pages
{
    public static class HtmlFormatHelper
    {
        public static IEnumerable<Tuple<string, TValue>> GetEnumData<TValue>()
            where TValue : Enum
        {
            var list = new List<Tuple<string, TValue>>();
            foreach (TValue value in Enum.GetValues(typeof(TValue)))
            {
                list.Add(new Tuple<string, TValue>(value.ToString(), value));
            }

            return list;
        }
    }
}

This helper class uses the Enum.GetValues method to enumerate each enum element and gets its name and value. It returns that information as a list of name-value tuples.

Additional Changes

As we’ve done in the other converter projects, we need to update the NavMenu and Program.cs file.

First, we add another NavLink for the Ultra Number Converter (lines #26-30).

@attribute [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">AppIdeas - Converters</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="bin2dec">
                <span class="oi oi-arrow-bottom" aria-hidden="true"></span> Bin2Dec
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="rom2dec">
                <span class="oi oi-move" aria-hidden="true"></span> Roman2Dec
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="number-converter">
                <span class="oi oi-loop-circular" aria-hidden="true"></span> Ultra Number Converter
            </NavLink>
        </li>
    </ul>
</div>

@code {
    private bool collapseNavMenu = true;

    private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

Then, we add the NumberConverterViewModel to the application’s dependency injection container (line #27).

using Blazor.AppIdeas.Converters.ViewModels;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace Blazor.AppIdeas.Converters
{
    [ExcludeFromCodeCoverage]
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            // add services and view models to DI container.
            builder.Services.AddTransient<RomanDecimalConverter>();
            builder.Services.AddTransient<NumberConverterViewModel>();

            await builder.Build().RunAsync();
        }
    }
}

With these changes complete, we can build and run the convert app again. Below is a screenshot of the Ultra Number Converter.

Fig 1 – Ultra Number Converter

Along with the project code, we also have a full set of unit and Blazor page tests. You can review the full set of tests with this commit on GitHub.

In conclusion, we built a converter that can go between multiple number system. With this project we learned:

  • To use the strategy pattern in our Model.
  • To use EditForm as a model-based replacement for <form> element.
  • To use Blazor components for input controls that support better databinding: <InputText> and <InputSelect>.
  • To build helper classes that help with page layout.
  • To embed layout logic in markup using the @ directive on Razor pages.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s