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.
- 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 sameConvert
system class. - 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.
- 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 .NETConvert
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 .NETConvert.ToString
method. - These methods use the condensed notation of switch expressions that was introduced in C#8.
- The
- 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);
}
}
}
}
- We start with properties for the entered number and its number system (binary, octal, decimal, hexadecimal, and roman).
- Then, we have properties for the result value and its number system.
- And, an error message (if an error happened during the conversion).
HasError
is a derived property that is true only when an error message is set.- 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 itsModel
attribute.EditForm
must bind to a Model or it will throw an exception when rendered. - The
InputText
component wraps the<input>
element. ItsValue
attribute property must also be bound to a property, and can only be used inside of anEditForm
. - Then, the
InputSelect
component (lines #11-12) implements binding and updates with itsValue
attribute. It is also a templated component, so we can use specific types (likeNumberSystem
) 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
andInputSelect
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 usingHtmlFormatHelper.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’sConvert
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.

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.