App-Idea 1: Bin2Dec

Our first project (Bin2Dec) will be a converter that takes binary numbers from input strings and converts them to their decimal value. And, we will add the ability to convert back from decimals to binary numbers as well.

There are several converters in the project list, so we will create a single Blazor app with multiple pages for each project idea to minimize the number of projects that we are creating and hosting. You can find the source for the Blazor.AppIdeas.Converters on GitHub. And the running sample of the Blazor app online.

We will start with a clean Blazor project by following the directions in the How-To: Create Blazor WASM Project article and name our project Blazor.AppIdeas.Converters.

BinaryDecimalConverter View Model

We are going to put our conversion logic into a separate class called BinaryDecimalConverter. This class represents the ViewModel in the Model-View-ViewModel (MVVM) design pattern, which is the glue and logic behind the View/Presentation (in Blazor that is represented by a component or page).

Let’s create the BinaryDecimalConverter class in the ViewModels folder.

using System;

namespace Blazor.AppIdeas.Converters.ViewModels
{
    public class BinaryDecimalConverter
    {
        public string Binary { get; set; }

        public string Decimal { get; set; }

        public string ErrorMessage { get; private set; }

        public string ErrorDisplay => string.IsNullOrEmpty(ErrorMessage) ? "none" : "normal";

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

                Decimal = Convert.ToInt32(Binary, 2).ToString();
            }
            catch
            {
                ErrorMessage = "Binary must be a valid number with only 0s and 1s.";
            }
        }

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

                int number = Convert.ToInt32(Decimal);
                Binary = Convert.ToString(number, 2);
            }
            catch
            {
                ErrorMessage = "Decimal must be a valid number with only digits 0-9.";
            }
        }
    }
}

This class has a straightforward implementation of methods that do the conversion between number systems:

  • First, we define properties for the user interface to bind with. Databinding is an important mechanism in MVVM (along with other patterns for presentation-logic separation), and Blazor supports that well.
    • Binary holds the binary number representation (line #7).
    • Decimal holds the integer number representation (line #9).
    • ErrorMessage is a display message to show whenever an error happens in the conversion (line #11).
    • ErrorDisplay is a derived property (line #13) that returns “none” when the the ErrorMessage is empty, and returns “normal” when the ErrorMessage is present. This allows us to toggle the user interface based on the presence of an error message.
  • The ConvertDecimal method (lines #15-28) takes the Binary property, converts it to an integer representation, and saves it as a string in the Decimal property.
  • The ConvertBinary method (lines #30-44) does the reverse. It takes the Decimal property, converts it into an integer, and converts it into a string in binary format into the Binary property.
  • Both methods have error handling logic to:
    • Start by hiding any previous error messages (lines #19 & 34).
    • Verify that the input strings are not null or empty (lines #20 & 35).
    • And handle any exceptions thrown by the Convert framework class (lines #24-27, 40-43).

With all of our conversion logic in a simple C# class, it will be easy to write unit tests for this logic isolated from the UI that it is running in. As a matter of fact, we could re-use this same class as the ViewModel in another application, like WPF, Windows 10, or Xamarin apps.

Binary-Decimal Convert Page

Blazor is an HTML/CSS based framework for building user interfaces. It provides additional attributes along with the standard HTML attributes to enable scenarios like binding to a property. Blazor also has some basic input components defined to help abstract away some of the HTML. But we can use either the Blazor components or the raw HTML/CSS elements. For this example, we will use the raw HTML/CSS.

Also for these projects we will mainly use the BootStrap CSS framework for defining layout. BootStrap comes with the default Blazor project, so it is the most convenient, though there are many such UI frameworks out there. So this article does require a working knowledge of Bootstrap 4.

Let’s create the BinaryDecimalConvert.razor page in the Pages folder.

@page "/bin2dec"

<div class="col-lg-8 col-md-10 offset-lg-2 offset-md-1 mt-3 pb-3 container">
    <h3 class="mt-3">Binary-Decimal Converter</h3>
    <hr />
    <form class="form-row">
        <div class="form-group col-lg-6 col-md-12">
            <label for="binary">Binary:</label>
            <input type="text" class="form-control" id="binary" 
                   placeholder="Enter binary number" @bind-value="@vm.Binary">
        </div>
        <div class="form-group col-lg-6 col-md-12">
            <label for="decimal">Decimal:</label>
            <input type="text" class="form-control" id="decimal" 
                   placeholder="Enter decimal number" @bind-value="@vm.Decimal">
        </div>
        <div class="alert alert-danger col-12 ml-1" style="display: @vm.ErrorDisplay">
            <strong>Error:</strong> @vm.ErrorMessage
        </div>
    </form>
    <div class="text-center">
        <input id="btn-convert-decimal" class="btn btn-outline-primary" type="button"
               value="Convert to Decimal" @onclick="vm.ConvertDecimal">
        <input id="btn-convert-binary" class="btn btn-outline-primary" type="button"
               value="Convert to Binary" @onclick="vm.ConvertBinary">
    </div>
</div>

@code {
    public BinaryDecimalConverter vm = new BinaryDecimalConverter();
}

For those developers comfortable with HTML/CSS/BootStrap, the layout section of this file will look very familiar… divs and classes and input elements. We basically layout two labels, two input elements, and two buttons to transform the input between them.

The @page "/bin2dec" directive (line #1) defines the Url route to this page. When the user navigates to this Url, Blazor routes the user to this page.

The @code section (lines #29-31) is where code is written in Razor pages. In this section, we only create an instance of the BinaryDecimalConverter class (from above) to use throughout this page. However any amount of page code can be written here. For this project and simplicity, we are directly creating an instance of our ViewModel. There are other options for tying together pages and ViewModels, which we will investigate in future projects.

Lines #17-18 show one-way databinding in Blazor@vm.ErrorDisplay sets the display style to the BinaryDecimalConverter.ErrorDisplay property. And, @vm.ErrorMessage binds to the content for a div. By doing this, the alert div is hidden and shown with an appropriate message depending on those properties.

Lines #22-25 also show one-way databinding, but this is binding an event (onclick) to a method call:

  • @onclick="vm.ConvertDecimal" binds the first button to call the ConvertDecimal, when it’s clicked.
  • @onclick="vm.ConvertBinary" binds the second button to call the ConvertBinary, when it’s clicked.

Finally, lines #10 & 15 show two-way databinding in Blazor. Two-way databinding means that changes to the input element are reflected in the bound property and vice-versa. @bind-value="@vm.Binary" dual binds the element’s value to the BinaryDecimalConverter.Binary property. And, we do the same for Decimal.

With these Blazor directives and bindings, we’ve taken static html and bound it to the code in our ViewModel, and defined the interactivity on the page. And, all of this C# code runs in the browser on the client.

Additional Change

There were a couple of more changes made for this project. The first is CSS additions for a couple of elements.

@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');

html, body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

a, .btn-link {
    color: #0366d6;
}

.btn-primary {
    color: #fff;
    background-color: #1b6ec2;
    border-color: #1861ac;
}

.content {
    padding-top: 1.1rem;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid red;
}

.validation-message {
    color: red;
}

#blazor-error-ui {
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

    #blazor-error-ui .dismiss {
        cursor: pointer;
        position: absolute;
        right: 0.75rem;
        top: 0.5rem;
    }

.container {
    border-style: solid;
    border-radius: 15px;
    border-width: 1px;
    border-color: darkgreen;
}

hr {
    border-top: 1px solid darkgreen;
}

The container CSS just puts a rounded, dark green border around the component. And the hr CSS makes the divider the same dark ground color.

And, we added a Bin2Dec link to the NavMenu (lines #16-20).

@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>
    </ul>
</div>

@code {
    private bool collapseNavMenu = true;

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

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

At this point, we can build and run the project locally. Enter some binary data (101) into the input element and click the ‘Covert to Decimal’ button to see the result (and covert back as well).

Fig 1 – Binary-Decimal Converter

Test Project

With the project complete, we are going to create some tests to verify the functionality too. Let’s start by creating a test project (named Blazor.AppIdeas.Converters.Tests project) in this same solution by following the steps in: How-to: Add bUnit Test Project to Blazor Solution article.

ViewModel Unit Tests

With our separate BinaryDecimalConverter class, it is easy to unit test the logic. Create a BinaryDecimalConverterTests class in the Blazor.AppIdeas.Converters.Tests project and ViewModels folder. We like our test project folders to match our source project, so it’s easy to navigate both projects.

using Blazor.AppIdeas.Converters.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace Blazor.AppIdeas.Converters.Tests.ViewModels
{
    public class BinaryDecimalConverterTests
    {
        private const string binaryErrorMessage = "Binary must be a valid number with only 0s and 1s.";
        private const string decimalErrorMessage = "Decimal must be a valid number with only digits 0-9.";

        [Fact]
        public void Construction()
        {
            // arrange

            // act
            var converter = new BinaryDecimalConverter();

            // assert
            Assert.NotNull(converter);
            Assert.Null(converter.Binary);
            Assert.Null(converter.Decimal);
            Assert.Null(converter.ErrorMessage);
            Assert.Equal("none", converter.ErrorDisplay);
        }

        [Theory]
        [InlineData("101", "5", null, "none")]
        [InlineData("11011010", "218", null, "none")]
        [InlineData("", null, binaryErrorMessage, "normal")]
        [InlineData("102", null, binaryErrorMessage, "normal")]
        public void ConvertDecimal_WithBinaryString(
            string initialBinary, string expectedDecimal, string expectedErrorMessage, string expectedErrorDisplay)
        {
            // arrange
            var converter = new BinaryDecimalConverter
            {
                Binary = initialBinary
            };

            // act
            converter.ConvertDecimal();

            // assert
            Assert.Equal(expectedDecimal, converter.Decimal);
            Assert.Equal(expectedErrorMessage, converter.ErrorMessage);
            Assert.Equal(expectedErrorDisplay, converter.ErrorDisplay);
        }

        [Theory]
        [InlineData("5", "101", null, "none")]
        [InlineData("186", "10111010", null, "none")]
        [InlineData("", null, decimalErrorMessage, "normal")]
        [InlineData("102l", null, decimalErrorMessage, "normal")]
        public void ConvertBinary_WithDecimalString(
            string initialDecimal, string expectedBinary, string expectedErrorMessage, string expectedErrorDisplay)
        {
            // arrange
            var converter = new BinaryDecimalConverter
            {
                Decimal = initialDecimal
            };

            // act
            converter.ConvertBinary();

            // assert
            Assert.Equal(expectedBinary, converter.Binary);
            Assert.Equal(expectedErrorMessage, converter.ErrorMessage);
            Assert.Equal(expectedErrorDisplay, converter.ErrorDisplay);
        }
    }
}

First, we notice the xUnit [Fact] and [Theory] attributes that are on each method. These attributes are used by the xUnit test runner to discover the unit tests in our class. [Fact] is for a single test method. [Theory] is used to define a method with variations that are run for each [InlineData] attribute on that same method. The method signature of [InlineData] must match the test method’s signature, and that data is passed into the method for each test.

All of the unit tests conform to the same outline: arrange (setup the requirements for the test), act (perform the test on the method), and assert (validate the state after the method is tested). This helps other developers and ourselves understand the intent of the tests.

The Construction test uses the [Fact] attribute because it is a single test. It creates the class and validates the starting state of its properties are what we expect.

The ConvertDecimal_WithBinaryString test is written to test various cases of converting binary to decimal numbers.

  1. It creates the BinaryDecimalConverter class with the initialBinary value set.
  2. It calls the ConvertDecimal method.
  3. It validates the expected results for the Decimal value, any error message text, and whether the error message should be displayed. These expected values are also passed into the test method.
  4. This method was generalized with parameters for the input and expected results, so that it can be run with different possible values.
  5. The [InlineData] defines individual test instances for Binary values of: 101, 11011010, “”, 102. The first two produce valid Decimal conversions. The last two exercise the error handling code and validate the expected error messages and display state.

The ConvertBinary_WithDecimalString test runs similar test variations, but instead we convert decimal values to binary numbers.

With these 3 tests and data variations, we fully cover the test scenarios needed to validate the functionality of our ViewModel class.

Page bUnit Tests

We can also create tests that validate the page functions as expected. bUnit is a unit test framework for rendering Blazor pages and components in a test context. It allows pages to be rendered and then validate the rendered HTML. It also allows us to perform actions on page elements.

Create the BinaryDecimalConvertTests class in the Blazor.AppIdeas.Converters.Tests project and Pages folder.

using Blazor.AppIdeas.Converters.Pages;
using Bunit;
using Xunit;

namespace Blazor.AppIdeas.Converters.Tests.Pages
{
    public class BinaryDecimalConvertTests
    {
        [Fact]
        public void InitialRender()
        {
            // arrange
            using var ctx = new TestContext();

            // act
            var cut = ctx.RenderComponent<BinaryDecimalConvert>();

            // assert
            cut.MarkupMatches(BinaryDecimalConvertExpectedResults.DefaultRenderResult);
            Assert.NotNull(cut.Instance.vm);
        }

        [Fact]
        public void DisplayErrorMessage()
        {
            // arrange
            using var ctx = new TestContext();
            var cut = ctx.RenderComponent<BinaryDecimalConvert>();

            // act
            cut.Find("#btn-convert-decimal").Click();

            // assert
            cut.MarkupMatches(BinaryDecimalConvertExpectedResults.BinaryErrorResult);
        }

        [Fact]
        public void ConvertToDecimal_Clicked()
        {
            // arrange
            using var ctx = new TestContext();
            var cut = ctx.RenderComponent<BinaryDecimalConvert>();
            cut.Instance.vm.Binary = "101";

            // act
            cut.Find("#btn-convert-decimal").Click();

            // assert
            cut.MarkupMatches(BinaryDecimalConvertExpectedResults.ConvertToDecimalResult);
        }

        [Fact]
        public void ConvertToBinary_Clicked()
        {
            // arrange
            using var ctx = new TestContext();
            var cut = ctx.RenderComponent<BinaryDecimalConvert>();
            cut.Instance.vm.Decimal = "7";

            // act
            cut.Find("#btn-convert-binary").Click();

            // assert
            cut.MarkupMatches(BinaryDecimalConvertExpectedResults.ConvertToBinaryResult);
        }
    }
}

First, we will notice these tests also follow the arrange-act-assert structure. Next, the tests are each very similar, they render the Blazor component or page, optionally perform an action on an element, and then validate the resulting HTML matches our expected HTML.

We will dive deep into the ConvertToDecimal_Clicked test to explain how bUnit works:

  1. We create a new instance of TestContext. This is the bUnit test context which works as the hosting context for our page. It simulates how Blazor works to construct and render a page or component.
  2. The RenderComponent method performs the render of a component (in our case the BinaryDecimalConvert page). In Blazor, pages are just components with routing information.
  3. We set the ViewModel.Binary property to “101” to simulate a user setting that in the Binary input element.
  4. With the page setup complete, we use IRenderedComponent<BinaryDecimalConvert>.Find to find the ‘Convert to Decimal’ button on the page. Using the "#btn-convert-decimal" text in the Find method searches for an element with that id. The Find method uses the same convention as CSS naming/matching.
  5. Then, we call the Click method on that element. This simulates the click event in the component/page element. By using these actions, we are simulating user interaction with the page. bUnit has a long list of actions that can be performed on elements.
  6. Finally, we use the IRenderedComponent<BinaryDecimalConvert>.MarkupMatches to assert that the currently rendered HTML matches our expected HTML. This is a semantic matcher, so the text doesn’t need to match exactly (attributes could be in different order for example), but the elements between the two must match.
  7. If MarkupMatches returns true, then we know the page rendering matches our expectations. If it returns false, then they don’t match. The tests results also show the elements and attributes that don’t match, so that we can narrow down the errors.

The remaining tests follow the same usage pattern, but either produce an error or do the reverse conversion. We test these variations to ensure all of our databindings are constructed correctly on the page.

Because the expected results for pages can be verbose, we also place all of them into separate class constants to simplify and improve the readability of the test methods. Create the BinaryDecimalConvertExpectedResults class in the Blazor.AppIdeas.Converters.Tests project and Pages folder. These constants are string literals of the HTML that we expect for each test case.

namespace Blazor.AppIdeas.Converters.Tests.Pages
{
    static class BinaryDecimalConvertExpectedResults
    {
        internal const string DefaultRenderResult =
@"    <div class=""col-lg-8 col-md-10 offset-lg-2 offset-md-1 mt-3 pb-3 container"">
      <h3 class=""mt-3"">Binary-Decimal Converter</h3>
      <hr>
      <form class=""form-row"">
        <div class=""form-group col-lg-6 col-md-12"">
          <label for=""binary"">Binary:</label>
          <input type=""text"" class=""form-control"" id=""binary"" placeholder=""Enter binary number"" />
        </div>
        <div class=""form-group col-lg-6 col-md-12"">
          <label for=""decimal"">Decimal:</label>
          <input type=""text"" class=""form-control"" id=""decimal"" placeholder=""Enter decimal number"" />
        </div>
        <div class=""alert alert-danger col-12 ml-1"" style=""display: none"">
          <strong>Error:</strong>
        </div>
      </form>
      <div class=""text-center"">
        <input id=""btn-convert-decimal"" class=""btn btn-outline-primary"" type=""button"" value=""Convert to Decimal"" />
        <input id=""btn-convert-binary"" class=""btn btn-outline-primary"" type=""button"" value=""Convert to Binary"" />
      </div>
    </div>
";

        internal const string BinaryErrorResult =
@"    <div class=""col-lg-8 col-md-10 offset-lg-2 offset-md-1 mt-3 pb-3 container"">
      <h3 class=""mt-3"">Binary-Decimal Converter</h3>
      <hr>
      <form class=""form-row"">
        <div class=""form-group col-lg-6 col-md-12"">
          <label for=""binary"">Binary:</label>
          <input type=""text"" class=""form-control"" id=""binary"" placeholder=""Enter binary number"" />
        </div>
        <div class=""form-group col-lg-6 col-md-12"">
          <label for=""decimal"">Decimal:</label>
          <input type=""text"" class=""form-control"" id=""decimal"" placeholder=""Enter decimal number"" />
        </div>
        <div class=""alert alert-danger col-12 ml-1"" style=""display: normal"">
          <strong>Error:</strong> Binary must be a valid number with only 0s and 1s.
        </div>
      </form>
      <div class=""text-center"">
        <input id=""btn-convert-decimal"" class=""btn btn-outline-primary"" type=""button"" value=""Convert to Decimal"" />
        <input id=""btn-convert-binary"" class=""btn btn-outline-primary"" type=""button"" value=""Convert to Binary"" />
      </div>
    </div>
";

        internal const string ConvertToDecimalResult =
@"    <div class=""col-lg-8 col-md-10 offset-lg-2 offset-md-1 mt-3 pb-3 container"">
      <h3 class=""mt-3"">Binary-Decimal Converter</h3>
      <hr>
      <form class=""form-row"">
        <div class=""form-group col-lg-6 col-md-12"">
          <label for=""binary"">Binary:</label>
          <input type=""text"" class=""form-control"" id=""binary"" placeholder=""Enter binary number"" value=""101"" />
        </div>
        <div class=""form-group col-lg-6 col-md-12"">
          <label for=""decimal"">Decimal:</label>
          <input type=""text"" class=""form-control"" id=""decimal"" placeholder=""Enter decimal number"" value=""5"" />
        </div>
        <div class=""alert alert-danger col-12 ml-1"" style=""display: none"">
          <strong>Error:</strong>
        </div>
      </form>
      <div class=""text-center"">
        <input id=""btn-convert-decimal"" class=""btn btn-outline-primary"" type=""button"" value=""Convert to Decimal"" />
        <input id=""btn-convert-binary"" class=""btn btn-outline-primary"" type=""button"" value=""Convert to Binary"" />
      </div>
    </div>
";

        internal const string ConvertToBinaryResult =
@"    <div class=""col-lg-8 col-md-10 offset-lg-2 offset-md-1 mt-3 pb-3 container"">
      <h3 class=""mt-3"">Binary-Decimal Converter</h3>
      <hr>
      <form class=""form-row"">
        <div class=""form-group col-lg-6 col-md-12"">
          <label for=""binary"">Binary:</label>
          <input type=""text"" class=""form-control"" id=""binary"" placeholder=""Enter binary number"" value=""111"" />
        </div>
        <div class=""form-group col-lg-6 col-md-12"">
          <label for=""decimal"">Decimal:</label>
          <input type=""text"" class=""form-control"" id=""decimal"" placeholder=""Enter decimal number"" value=""7"" />
        </div>
        <div class=""alert alert-danger col-12 ml-1"" style=""display: none"">
          <strong>Error:</strong>
        </div>
      </form>
      <div class=""text-center"">
        <input id=""btn-convert-decimal"" class=""btn btn-outline-primary"" type=""button"" value=""Convert to Decimal"" />
        <input id=""btn-convert-binary"" class=""btn btn-outline-primary"" type=""button"" value=""Convert to Binary"" />
      </div>
    </div>
";
    }
}

Build the entire solution and run the new tests. They should all run successfully to validate our first project.

With the code and tests done, we have completed this first project. While this is relatively easy functionality, we learned how to:

  • Use raw HTML/CSS on a Blazor page to produce its layout.
  • Create our logic in a separate class.
  • Use databinding (one or two-way) to show and edit data on the page.
  • Call methods in response to element events.
  • Write tests to validate the HTML produced by a Blazor page.

4 thoughts on “App-Idea 1: Bin2Dec

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