Skip to content

Latest commit

 

History

History
641 lines (471 loc) · 28.8 KB

File metadata and controls

641 lines (471 loc) · 28.8 KB

3.2.1 Web Applications

3.2.1.0 Introduction

Web applications are the most common type of exposer components today. They are much easier to use than other known exposer UI components in the software industry. The web software market is also much easier for engineers to publish and update than mobile applications, making it attractive for newer engineers. But more importantly, web applications have a much more diverse set of technologies than mobile applications.

This chapter will use Blazor technology to demonstrate implementing the Standard principles for web applications. However, as mentioned previously, the Standard is technology-agnostic, meaning it can be applied to any web technology without issues.

3.2.1.1 On the Map

Web applications are usually set at the other end of any system. They are the terminals that humans use to interact with the system. Let's take a look at where they are located on the map:



As shown above, web applications are similar to core APIs, except that they have a different group of components in terms of visualization, such as Pages, Components, and Bases. There's an intersection between two main flows in every web application. The presentation flow and the data/business flow. Depending on where a web application lives in terms of high-level architecture, its location determines whether its backend (BFF or Backend of Frontend) is a business flow or just data flow. Let's discuss these details in the characteristics section of this chapter.

3.2.1.2 Charactristics

Brokers, Services, View Services, Bases, Components, and Pages. Web applications usually have six essential components. Since we've already discussed the data flow components in the Services portion of The Standard, this section will discuss the UI aspect (Bases, Components, and Pages) with a slight detail about view services.

Let's discuss these characteristics here.

3.2.1.2.0 Anatomy

UI components consist of base components, core components, and pages. They all separate the responsibility of integration, rendering, and routing users to a particular UI functionality.

Let's talk about these types in detail.

3.2.1.2.0.0 Base Component

Base components are just like brokers. They are wrappers around native or external UI components. Their primary responsibility is to abstract away any hard dependency on non-local UI capability. Let's say we want to offer the ability to create text boxes for data insertion/capture. The native <input> tag could offer this capability. However, exposing or leveraging this tag in our core UI components is dangerous. Because it creates a hard dependency on non-abstract UI components, if we decide to use some 3rd party UI component at any point, we would need to change these native <input> tags across all the components that use them. That's not an optimum strategy.

Let's take a look at a visualization for base component functionality:



As seen in the above example, base components will wrap an external or native UI component and then expose APIs to seamlessly and programmatically interact with that Component. Occasionally, these APIs will represent parameters, functions, or delegates to interact with the Component based on the business flow.

3.2.1.2.0.0.0 Implementation

Let's take a look at a simple Base component for solving this problem:

<input @bind-value=Value />
public partial class TextBoxBase : ComponentBase
{
    [Parameter]
    public string Value {get; set;}

    public void SetValue(string value) =>
        this.Value = value;
}

In the code above, we wrapped the <input> tag with our base component TextBoxBase and offered an input parameter Value to be passed into that Component so it can pass it down to the native UI element. Additionally, we provided a public function, SetValue to allow for programmatically mimicking the user's behavior to test drive the consuming Component of this base element.

3.2.1.2.0.0.1 Utilization

Now, when we try to leverage this base component at the core components level, we can call it as follows:

<TextBoxBase @ref=MyTextBox />

The @ref aspect will allow the backend code to interact with the base component programmatically behind the scenes to call any existing functionality.

3.2.1.2.0.0.2 Restrictions

Components can only use base components. Pages may not use them, and other Base components may not use them. But more importantly, it's preferred those base components would only wrap around one and only one non-local Component.

And just like Brokers, Base Components do not have any business logic. They don't handle exceptions, do any calculations, or any form of sequential, iterative, or selective business logic operations. These operations are either data-based, where they belong to view services and downstream APIs, or UI-based, where they belong to Core Components.

Base components also don't handle exceptions, they don't throw their exceptions, and they don't perform any validations.

3.2.1.2.0.1 Core Component

Core components are just like services in the data flow. They are test-driven, but they are also restricted to one and only one dependency at all times. Core components leverage Base components to perform a business-specific flow. They are less generic than Base components because they orchestrate and communicate with a very particular service for the data flow.

Here's a visualization of core components architecture:



Core components are UI and Data component orchestrators. They will leverage one or many Base components to construct a business-specific flow, such as a student registration form, then send the signal to view services to persist that data and return responses or report errors.

Core Components are three main parts. Elements, Styles, and Actions. Let's discuss these parts here:

3.2.1.2.0.1.0 Elements

Elements are mainly the markup pieces you find in the .razor file in any component. These elements should always be Base Components. They are the skeleton of any Core Component. These Elements may or may not expose sub-routines, such as a Button Click, or a reactionary routine, such as a Button color change on hover, and so on.

Elements can be tested in three main ways. Existence, Properties, and Actions.

3.2.1.2.0.1.0.0 Existence

First and foremost, we need to ensure the Element is loaded and is present on the screen. This can be done in three different ways. Either by property assignment, searching by id, or searching for all types. Here are some examples:

3.2.1.2.0.1.0.0.0 Property Assignment

Every Component should have a corresponding property attached to the Element at runtime. For instance, assume we have a StudentRegistrationComponent as follows:

public class StudentRegistrationComponent: ComponentBase
{
    public TextBoxBase NameTextBox {get; set;}
}

In the above code, we defined NameTextBox as the same type as the Base Component that will be attached to it. Once that property is defined, we will need to write a failing test that verifies that this Element exists as follows:

public void ShouldLoadNameTextBox()
{
    // when
    this.renderedStudentRegistrationComponent =
        RenderComponent<StudentRegistrationComponent>();

    // then
    this.renderedStudentRegistrationComponent.Instance.NameTextBox
        .Should().NotBeNull();
}

The above test will fail. That's simply because no markup corresponds to the NameTextBox property on rendering-time. Let's make this test pass by changing the markup in StudentRegistrationComponent.razor as follows:

<TextBoxBase @ref=NameTextBox>

Our test will now pass. That's simply because the property is dynamically instantiated at render time once the page loads.

3.2.1.2.0.1.0.0.1 Searching by Id

Sometimes, Property Assignment is not an option. There are scenarios where components load dynamically a set of nested components that we may not have access to at design time. In this case, searching by ID is our best option to ensure we have the right Component in hand.

Here's an example. Assume we have a list of components that loads dynamically by being given a list of students. We use the student object Id as an identifier for every Component. Our code looks as follows:

public partial StudentListComponent : ComponentBase
{
    public List<Student> Students {get; set;}

    ....

    public void OnIntialized() =>
        Students = await this.someStudentViewService.RetrieveAllStudentsAsync();
}

On load - we call a view service to pull a list of all students asynchronously. We need to take that list and dynamically load a nested view for each student. Let's write a failing test for this first:

public void ShouldLoadStudentsAsync()
{
    // given
    List<Student> randomStudents = CreateRandomStudents();
    ...

    this.someStudentViewService.Setup(service =>
        service.RetrieveAllStudentAsync())
            .ReturnsAsync(randomStudents);

    // when
    this.renderedStudentListComponent =
        RenderComponent<StudentRegistrationComponent>();

    // then
    ....

    foreach(Student student in randomStudents)
    {
        StudentComponent studentComponent =
            this.renderedStudentListComponent.Find($"#{student.Id}")
                as StudentComponent;

        studentComponent.Should().NotBeNull();
    }
    
    ...
}

In the above tests, we looked for components that matched the student ID, and verified they existed. Let's make that test pass as follows:

<Iterations Items="Students">
  <StudentComponent Value="@context" />
</Iterations>>

We use the PrettyBlazor library to markup our iteration behavior with the <Iterations> tag. Now, our tests should pass by finding and verifying each created Component once they load on the screen.

3.2.1.2.0.1.0.0.2 General Search

There are scenarios where we don't have a key or an Id to find the Element. We expect a list of "things" to load on the screen without any data or information on them. In this case, we are going to have to resolve the General search mechanism where we rely on the count of the rendered components against the count that we expect as follows:

public void ShouldLoadManyElements()
{
    // given
    int randomCount = GetRandomNumber();

    // when
    this.renderedThingsComponent =
        RenderComponent<StudentRegistrationComponent>();

    // then
    var renderedThings = this.renderedThingsComponent.Find("p");

    renderedThings.Count.Should().Be(randomCount);
}

The Standard advises against having unknown-typed components like these loaded on the screen as they give engineers much less control over what's going on. But in gaming scenarios, this could be the only option.

3.2.1.2.0.1.0.1 Properties

The other aspect we consider when developing Core Components is their properties. These could be properties of the Core Component itself or the Base Component. For instance, we want to verify that a LabelBase component has property information such as First Name or Last Name.

Let's start by setting up a test.

public class StudentRegistrationComponent: ComponentBase
{
    public LabelBase FirstNameLabel {get; set;}
}

In the above code, our StudentRegistration component has a label on the screen that is supposed to have a certain value by default for a form. Let's write a failing test for it as follows:

public void ShouldHaveFirstNameLabel()
{
    // given
    string expectedFirstNameLabel = "First Name";

    // when
    this.renderedStudentRegistrationComponent =
        RenderComponent<StudentRegistrationComponent>();

    // then
    ...
    this.renderedStudentRegistrationComponent.Instance.FirstNameLabel.Value
        .Should().Be(expectedFirstNameLabel);

    ...
}

The test here will verify the label will always have the property value First Name. Let's make it pass.

<LabelBase @ref=FirstNameLabel Value="First Name">

We verified that the Element exists with the right property or information by simply doing that.

The same thing applies to properties on the Core Component itself, like having view models that load on initialization and then get assigned to certain base components. We will show that example shortly.

3.2.1.2.0.1.0.2 Actions

Testing actions is one of the most important parts of testing any Element. We want to ensure that a certain action is triggered when a button is clicked. These actions can also change a property, create a new element, or trigger another action. There are as many possibilities as there are in the very pattern of Tri-Nature itself.

Let's assume our StudentRegistrationComponent is supposed to trigger a call for a StudentViewService on the Button click event. Let's start with a simple failing test as follows:

[Fact]
public void ShouldSubmitStudent()
{
    // given
    StudentView randomStudentView = CreateRandomStudentView();
    ...

    // when
    this.renderedStudentRegistrationComponent =
        RenderComponent<StudentRegistrationComponent>();

    this.renderedStudentRegistrationComponent.Instance.SubmitButton.Click();

    // then
    this.studentViewServiceMock.Verify(service =>
        service.AddStudentViewAsync(
            this.renderedStudentRegistrationComponent.Instance.StudentView),
                Times.Once);

    ...
}

In the above test, we propose implementing a component that will trigger calling AddStudentViewAsync from a StudentViewService once the button clicks. This implies a correlation between clicking a button and triggering an action.

Let's write an implementation for this behavior. On the component code side, we should have the following function as follows:

public partial class StudentRegistrationComponent : ComponentBase
{
    [Inject]
    public IStudentViewService StudentViewService { get; set; }
    ...
    public StudentView StudentView { get; set; }
    public ButtonBase SubmitButton { get; set; }
    ...

    public async void RegisterStudentAsync() =>
        await this.StudentViewService.AddStudentViewAsync(this.StudentView);
}

The above code implements a RegisterStudentAsync function that will pass StudentView property (data) unto the StudentViewService for registration/add. Now, let's attach that function to a UI element on the markup side as follows:

<ButtonBase @ref=@SubmitButton
            Label="SUBMIT"
            OnClick=@RegisterStudentAsync />

In the above markup, we attached the SubmitButton property to the Element and passed the OnClick event with the RegisterStudentAsync routine. When the button is clicked, the routine will trigger, and we should be able to verify it in our unit tests.

3.2.1.2.0.1.1 Styles

Core Components also carry more than just elements. They have certain styles to ensure the user experience fits the type of business they're trying to accomplish. While Elements or Base Components can also carry their own styles, it's important to realize that styles are better suited to Core Components to ensure the modularity of Base Components to fit whatever style is enforced by Core Components.

Testing styles are rare in the UI world. Especially when it comes to test-driving styles in C# as code. The Standard enforces the idea of leveraging the same programming language (when possible) across all different aspects of a project. That also includes infrastructure, pipelines, styles, actions, and everything else in between. This principle ensures that the learning curve for engineers working on any project is as minimal as possible, in addition to having standardized patterns.

We will leverage a library called SharpStyles to test styles on Core Components. The library flawlessly translates C# code into CSS styles.

Let's consider a scenario where we want our SubmitButton on the registration component above to have a blue color for its background. Let's add a Style property on our Component as follows:

First of all, we need to create a C# model with the identifiers we would like to have in our CSS style as follows:

public class StudentRegistrationStyle : SharpStyle
{
    [CssClass]
    public SharpStyle SubmitButton { get; set; }
}

This model will be translated into a CSS class called submit-button when we start rendering the Component. Let's leverage this new model in our Component as follows:

public partial class StudentRegistrationComponent : ComponentBase
{
        public StyleBase StyleElement { get; set; }
        public StudentRegistrationStyle StudentRegistrationStyle { get; set; }
        ...
}

Now that we have a new property for styles, we need to hook this property to a markup that will transform these styles/models into pure native CSS. We will need to create a StyleBase Element/Base Component to take care of the abstraction side for us - so we don't have any hard dependency on the SharpStyle library as follows:

The markup side of that will look as follows:

<style>
    @Style.ToCss()
</style>

The code side of the same Element/Base Component will be as follows:

public partial class StyleBase : ComponentBase
{
    [Parameter]
    public SharpStyle Style { get; set; }
}

Now, let's go ahead and utilize this Base Component in our StudentRegistrationComponent as follows:

<StyleBase @ref=StyleElement
           Style=StudentRegistrationStyle />

Now that we have everything setup, let's write a failing test to require a button to have a blue background color as follows:

[Fact]
public void ShouldRenderContainerWithStyles()
{
    // given
    string expectedCssClass = "submit-button";
    ...
    var expectedStyle = new StudentRegistrationStyle
    {
        SubmitButton = new SharpStyle
        {
            BackgroundColor = "blue"
        },
    };

    // when
    this.renderedStudentRegistrationComponent =
        RenderComponent<StudentRegistrationComponent>();

    // then
    this.renderedLabOverviewComponent.Instance.SubmitButton.CssClass
        .Should().BeEquivalentTo(expectedCssClass);

    this.renderedStudentRegistrationComponent.Instance.StudentRegistrationStyle
        .Should().BeEquivalentTo(expectedStyle);

    this.renderedStudentRegistrationComponent.Instance.StyleElement.Style
        .Should().BeEquivalentTo(expectedStyle);
}

With a failing test like this, we can now start writing an implementation to satisfy the following conditions for this test.

On the component code side, let's generate the expected style object:

public partial class StudentRegistrationComponent : ComponentBase
{
    public StyleBase StyleElement { get; set; }
    public StudentRegistrationStyle StudentRegistrationStyle { get; set; }
    ...
    protected override void OnInitialized() => SetupStyles();

    public void SetupStyles()
    {
        this.StudentRegistrationStyle = new StudentRegistrationStyle
        {
            SubmitButton = new SharpStyle
            {
                BackgroundColor = "blue"
            },
        };
    }
    ...
}

Then, on the markup side, let's attach all the properties to their respective Elements as follows:

<StyleBase @ref=StyleElement
           Style=StudentRegistrationStyle />

<ButtonBase CssClass="submit-button" ... />

Our tests should pass, and this would be a quick demonstration of a standardized way of testing styles for UI components.

3.2.1.2.0.1.2 Actions

Actions in Core Components are very similar to Actions in Base Components or Elements. It is important, however, to understand that every action can easily be verified by either changing a property or style, creating other components, or simply triggering other actions. It can also be a combination of one or many of those above. For instance, a submit button could change the properties of existing elements by making them disabled while triggering a call/action to another service. It should all be verifiable, as we discussed above.

3.2.1.2.0.1.0 Full Implementation & Tests

Let's take a look at the implementation of a core component.

public partial class StudentRegistrationComponent : ComponentBase
{
    [Inject]
    public IStudentViewService StudentViewService {get; set;}

    public StudentRegistrationComponentState State {get; set;}
    public StudentView StudentView {get; set;}
    public TextBoxBase StudentNameTextBox {get; set;}
    public ButtonBase SubmitButton {get; set;}
    public LabelBase StatusLabel {get; set;}

    public void OnIntialized() =>
        this.State == StudentRegisterationComponentState.Content;

    public async Task SubmitStudentAsync()
    {
        try
        {
            this.StudentViewService.AddStudentViewAsync(this.StudentView);
        }
        catch (Exception exception)
        {
            this.State = StudentRegisterationComponentState.Error;
        }
    }
}

The above code shows the different types of properties within any given component�the dependency view service maps raw API models/data into consumable UI models. The State determines whether a component should be Loading, Content, or Error. The data view model binds incoming input to one unified model, StudentView. The last three are base-level components used to construct the form of registration.

Let's take a look at the markup side of the core component:

<Condition Evaluation=IsLoading>
    <Match>
        <LabelBase @ref=StatusLabel Value="Loading ..." />
    </Match>
</Condition>

<Condition Evaluation=IsContent>
    <Match>
        <TextBoxBase @ref=StudentNameTextBox @bind-value=StudentView.Name />
        <ButtonBase @ref=SubmitButton Label="Submit" OnClick=SubmitStudentAsync />
    </Match>
</Condition>

<Condition Evaluation=IsError>
    <Match>
        <LabelBase @ref=StatusLabel Value="Error Occurred" />
    </Match>
</Condition>

We linked the references of the student registration component properties to UI components to ensure the rendering of these components and data submission execution.

A component has already loaded state and post-submission states. Let's look at a couple of tests to verify these states.

[Fact]
public void ShouldRenderComponent()
{
    // given
    StudentRegisterationComponentState expectedComponentState =
        StudentRegisterationComponentState.Content;

    // when
    this.renderedStudentRegistrationComponent =
        RenderComponent<StudentRegistrationComponent>();

    // then
    this.renderedStudentRegistrationComponent.Instance.StudentView
        .Should().NotBeNull();

    this.renderedStudentRegistrationComponent.Instance.State
        .Should().Be(expectedComponentState);

    this.renderedStudentRegistrationComponent.Instance.StudentNameTextBox
        .Should().NotBeNull();

    this.renderedStudentRegisterationComponent.Instance.SubmitButton
        .Should().NotBeNull();

    this.renderedStudentRegistrationComponent.Instance.StatusLabel.Value
        .Should().BeNull();

    this.studentViewServiceMock.VerifyNoOtherCalls();
}

The test above will verify that all the components are assigned a reference property and that no external dependency calls have been made. It will also validate that the code in the OnIntialized function on the component level is validated and performing as expected.

Now, let's take a look at the submittal code validations:

[Fact]
public void ShouldSubmitStudentAsync()
{
    // given
    StudentRegisterationComponentState expectedComponentState =
        StudentRegisterationComponentState.Content;

    var inputStudentView = new StudentView
    {
        Name = "Hassan Habib"
    };

    StudentView expectedStudentView = inputStudentView;

    // when
    this.renderedStudentRegistrationComponent =
        RenderComponent<StudentRegistrationComponent>();

    this.renderedStudentRegistrationComponent.Instance.StudentName
        .SetValue(inputStudentView.Name);

    this.renderedStudentRegistrationComponent.Instance.SubmitButton.Click();

    // then
    this.renderedStudentRegistrationComponent.Instance.StudentView
        .Should().NotBeNull();

    this.renderedStudentRegisterationComponent.Instance.StudentView
        .Should().BeEquivalentTo(expectedStudentView);

    this.renderedStudentRegistrationComponent.Instance.State
        .Should().Be(expectedComponentState);

    this.renderedStudentRegistrationComponent.Instance.StudentNameTextBox
        .Should().NotBeNull();

    this.renderedStudentRegistrationComponent.Instance.StudentNameTextBox.Value
        .Should().BeEquivalentTo(studentView.Name);

    this.renderedStudentRegisterationComponent.Instance.SubmitButton
        .Should().NotBeNull();

    this.renderedStudentRegistrationComponent.Instance.StatusLabel.Value
        .Should().BeNull();

    this.studentViewServiceMock.Verify(service =>
        service.AddStudentAsync(inputStudentView),
            Times.Once);

    this.studentViewServiceMock.VerifyNoOtherCalls();
}

The test above validates that on submittal, the student model is populated with the data set programmatically through the base component instance and verifies all these components are rendered on the screen before end-users by validating each base component an assigned instance on runtime or render-time.

3.2.1.2.0.1.1 Restrictions

Core components have similar restrictions to Base components because they cannot call each other at that level. There's a level of Orchestration Core Components that can combine multiple components to exchange messages. Still, they don't render anything independently, the same way Orchestration services delegate all the work to their dependencies.

One view service corresponds to one core component, which renders one and only one view model. However, core components are also not allowed to call more than one view service. And in that, they always stay true to the view model.

View services may do their orchestration-level work in an extremely complex flow, but we recommend keeping things at a flat level. These same view services perform nothing but mapping and adding audit fields and basic structural validations.

3.2.1.2.0.2 Pages

In every web application, pages are a fundamental mandatory container component that needs to exist so end-users can navigate to them. Pages mainly hold a route, communicate a parameter from that route, and combine core-level components to represent a business value.

An excellent example of a page is a dashboard. Dashboard pages contain multiple components, such as tiles, notifications, headers, and sidebars, that reference other pages. Pages don't hold any business logic in and of themselves, but they delegate all route-related operations to their child components.

Let's take a look at a simple page implementation:

@page '/registration'

<HeaderComponent />
<StudentRegisterationComponent />
<FooterComponent />

Pages are much simpler than core or base components. They don't require unit testing and don't necessarily need backend code. They purely reference their components without reference (unless required) and help serve that content when navigating via a route.

3.2.1.2.0.3 Unobtrusiveness

It's a violation to include code from multiple technologies/languages on the same page for all UI components. For instance, CSS style code, C# code, and HTML markup cannot all exist in the same file. They need to be separated into their own files.

The unobtrusiveness rule helps prevent cognitive pollution for engineers building UI components and makes the system much easier to maintain. That's why every Component can nest its files beneath it if the IDE/Environment used for development allows for partial implementations as follows:

  • StudentRegisterationComponent.razor
    • StudentRegisterationComponent.razor.cs
    • StudentRegisterationComponent.razor.css

The node file here, .razor, has all the markup needed to kick off the Component's initialization. At the same time, both nested files are supporting files for simple UI logic code and styling. This level of organization (especially in Blazor) doesn't require any referencing for these nested/support files. Still, this may not be the case for other technologies, so I urge engineers to do their best to fit that model/Standard.

3.2.1.2.0.4 Organization

All UI components are listed under a Views folder in the solution. Let's take a look:

  • Views
    • Bases
    • Components
    • Pages

This tri-nature conforming organization should make it easier to shift reusable components and make it also easier to find these components based on their categories. Given the nesting is in place, I will leave it up to the engineers' preference to break down these components further by folders/namespaces or leave them all at the same level.