Skip to content

Task based ASP ,NET Components

Maurice CGP Peters edited this page Nov 14, 2020 · 8 revisions

Writing a component

Let's start with a example. The code shown here is an example of a task based component. The task we are trying to accomplish here is adding an inventory item.

A component has 2 required methods:

  1. A View method and, not surprisingly
  2. An Update method

These directly correspond to the concepts of the 'Model View Update' pattern. To add an inventory item, we must create and send a command. In this case the InventoryItemCommand . The command received its input parameters in the View method as a result of user interaction with that view. This is done via the Dispatch method. This method expects an Aggregate (in this case an InventoryItem) that can handle the command and the, validated, command itself as an input. When the command is dispatched, it will be handled by the aggregate in the normal way. As a result, either it calls the Update method of the component or it will set the 'Errors' property of the component. The Update method is responsible for updating the state of the user interface. The actual rendering is triggered by the Component base class. During this trigger the 'View' method is called to refresh the user interface so that it reflects the latest state.

[Route("/Add")]
    public class AddInventoryItemComponent : Component<AddInventoryItemViewModel, InventoryItemCommand, InventoryItemEvent, Json>
    {

        protected override Update<AddInventoryItemViewModel, InventoryItemEvent> Update { get; } = (state, events) =>
        {
            return events.Aggregate(
                state,
                (model, @event) =>
                {
                    switch (@event)
                    {
                        case InventoryItemCreated created:
                            state.InventoryItemCount = created.Count;
                            state.InventoryItemName = created.Name;
                            break;
                    }

                    return state;
                });
        };

        protected override Node View(AddInventoryItemViewModel currentViewModel) => concat(
            h1(NoAttributes(), text("Add new item")),
            div(
                new[] {@class("form-group")},
                Elements.label(
                    new[] {@for("idInput")},
                    text("Id")),
                input(
                    @class("form-control"),
                    id("idInput"),
                    bind.input(currentViewModel.InventoryItemId, id => currentViewModel.InventoryItemId = id)),
                Elements.label(
                    new[] {@for("nameInput")},
                    text("Name")),
                input(
                    @class("form-control"),
                    id("nameInput"),
                    bind.input(currentViewModel.InventoryItemName, name => currentViewModel.InventoryItemName = name)),
                Elements.label(
                    new[] {@for("countInput")},
                    text("Count")),
                input(
                    @class("form-control"),
                    id("countInput"),
                    bind.input(currentViewModel.InventoryItemCount, count => currentViewModel.InventoryItemCount = count)
                )),
            button(
                new[]
                {
                    @class("btn btn-primary"), on.click(
                        async args =>
                        {
                            Validated<InventoryItemCommand> validCommand = CreateInventoryItem.Create(
                                currentViewModel.InventoryItemId,
                                currentViewModel.InventoryItemName,
                                true,
                                currentViewModel.InventoryItemCount);

                            Aggregate<InventoryItemCommand, InventoryItemEvent> inventoryItem = BoundedContext.Create(InventoryItem.Decide, InventoryItem.Update);
                            Option<Radix.Error[]> result = await Dispatch(inventoryItem, validCommand);
                            switch (result)
                            {
                                case Some<Radix.Error[]>(_):
                                    if (JSRuntime is not null)
                                    {
                                        await JSRuntime.InvokeAsync<string>("toast", Array.Empty<object>());
                                    }

                                    break;
                                case None<Radix.Error[]> _:
                                    NavigationManager.NavigateTo("/");
                                    break;
                            }
                        })
                },
                text("Ok")
            ),
            navLinkMatchAll(new[] {@class("btn btn-primary"), href("/")}, text("Cancel")),
            div(
                NoAttributes(),
                div(
                    new[] {@class("toast"), attribute("data-autohide", "false")},
                    div(
                        new[] {@class("toast-header")},
                        strong(new[] {@class("mr-auto")}, text("Invalid input")),
                        small(NoAttributes(), text(DateTimeOffset.UtcNow.ToString(CultureInfo.CurrentUICulture))),
                        button(new[] {type("button"), @class("ml-2 mb-1 close"), attribute("data-dismiss", "toast")}, Elements.span(NoAttributes(), text("🗙")))),
                    div(
                        new[] {@class("toast-body")},
                        FormatErrorMessages(currentViewModel.Errors)
                    ))));

        private static IEnumerable<IAttribute> NoAttributes() => Enumerable.Empty<IAttribute>();

        private static Node FormatErrorMessages(IEnumerable<Radix.Error> errors)
        {
            Node node = new Empty();
            if (errors is not null)
            {
                node = ul(Array.Empty<IAttribute>(), errors.Select(error => li(Array.Empty<IAttribute>(), text(error.ToString()))).ToArray());
            }

            return node;
        }
    }

Creating a strongly typed user interface

As you probably have noticed, there is no Razor syntax involved. HTML markup is generated by strongly typed functions that represent elements, attributes, events and other components.

The above code will generate the following markup:

        <h1>Add new item</h1>
        <div class="form-group">
            <label for="idInput">Id</label>
            <input class="form-control" id="idInput">
            <label for="nameInput">Name</label>
            <input class="form-control" id="nameInput">
            <label for="countInput">Count</label>
            <input class="form-control" id="countInput">
        </div>
        <button class="btn btn-primary">Ok</button>
        <!--!-->
        <a href="/" class="btn btn-primary">Cancel</a>
        <div>
            <div class="toast" data-autohide="false">
                <div class="toast-header">
                    <strong class="mr-auto">Invalid input</strong>
                    <small>11/13/2020 5:55:27 PM +00:00</small>
                    <button type="button" class="ml-2 mb-1 close" data-dismiss="toast">
                        <span>🗙</span>
                    </button>
                </div>
                <div class="toast-body">

                </div>
            </div>
        </div>