Dashboard Modularity using Tuxboard

June 24th, 2024

In this post, we look at breaking up our dashboard into more modular components for easier access

What's Next?

Full examples located at Tuxboard.Examples.

Since we now understand how Tuxboard is organized into sections, let's shift our focus to the layout of the dashboard itself and how we can make it a little more modular for our needs.

In one example, everything loads through the Index.cshtml page. While this approach is very simplistic, there are a couple reasons why we should avoid this type of dashboard:

  1. The rendering of the page occurs all at once and then displays the dashboard. If a user has a lot of widgets, it'll take a while to render the page for processing causing the user to wait.
  2. It just looks messy.

What we need is to make the dashboard a little more modular. This means breaking the dashboard into multiple ViewComponents. We want to present the user with the skeleton of a dashboard and then load the widget's content dynamically. There is a method to the madness, but using ViewComponents has a number of benefits.

Important Note: If you notice the code below, there are places where ID's are being displayed (id="@Model.DashboardId"). If you are creating a public/static dashboard, REMOVE the id's from the HTML. The ID's are specific to when users log in to their own customized dashboard and used to identify when widgets are moved using TypeScript/JavaScript. DO NOT use them when displaying a public dashboard. This could be a potential security issue. 

Creating the Tuxboard Template

If you look at a simple dashboard project, you'll notice the Index.cshtml page contains everything which essentially slows down the rendering of the page. We want to break this down into it's own ViewComponents.

We'll breakdown it down by building a TuxboardTemplate ViewComponent as our top-level component.

  1. Create a TuxboardTemplate folder under Shared\Components
  2. Add a Default.cshtml and a TuxboardTemplateViewComponent class file.

Shared\Components\TuxboardTemplate\Default.cshtml

@model Tuxboard.Core.Domain.Entities.Dashboard

<section class="dashboard" id="@Model.DashboardId">
    @foreach (var tab in Model.Tabs)
    {
        foreach (var layout in tab.Layouts)
        {
            @await Component.InvokeAsync("layouttemplate", layout)
        }
    }
</section>

Shared\Components\TuxboardTemplate\TuxboardTemplateViewComponent.cs

[ViewComponent(Name="tuxboardtemplate")]
public class TuxboardTemplateViewComponent : ViewComponent
{
    public IViewComponentResult Invoke(Dashboard model)
    {
        return View(model);
    }
}
  1. Confirm you have the "@addTagHelper *, <yourProjectName>" in your _ViewImports.cshtml file.
  2. Remove all of your code from the Index.cshtml and replace it with the following snippet below:
    <vc:tuxboardtemplate model="Model.Dashboard"></vc:tuxboardtemplate>
    Since you are already passing in a full Dashboard model, this will simply pass it into the ViewComponent.

A couple notes regarding the code:

If you decide to run this, you'll get an error saying it can't find the LayoutTemplateViewComponent. We need to create that ViewComponent next.

Creating the Layout Template

The layout template is the next ViewComponent to create. The Layout Template contains the rows and columns of our dashboard along with our widgets and their positions sorted by index. It's almost exactly the same as the Tuxboard Template.

  1. Create a LayoutTemplate folder under Shared\Components
  2. Add a Default.cshtml and a LayoutTemplateViewComponent class file.

Shared\Components\LayoutTemplate\Default.cshtml

@model Tuxboard.Core.Domain.Entities.Layout

<form method="post">

    <div class="dashboard-tab" data-id="@Model.TabId">

        @foreach (var row in Model.LayoutRows.OrderBy(e => e.RowIndex))
        {
            <div class="layout-row" data-id="@row.LayoutRowId">

                @foreach (var col in row.GetColumnLayout())
                {
                    <div class="column @col.ColumnClass">

                        @foreach (var wp in row.WidgetPlacements.Where(y => y.ColumnIndex == col.Index).OrderBy(e => e.WidgetIndex))
                        {
                            @await Component.InvokeAsync("widgettemplate", wp)
                        }
                    </div>
                }
                <div class="clearfix"></div>
            </div>
        }
        <div class="clearfix"></div>
    </div>
</form>

Shared\Components\LayoutTemplate\LayoutTemplateViewComponent.cs

[ViewComponent(Name="layouttemplate")]
public class LayoutTemplateViewComponent : ViewComponent
{
    public IViewComponentResult Invoke(Layout layout)
    {
        return View(layout);
    }
}

The LayoutTemplate is using the Layout class to populate the dashboard. First, the row is displayed ordered by the .RowIndex property. We, then proceed to create each column through the .GetColumnLayout() method by Layout property containing CSS classes delimited by a slash (/). The property .ColumnClass is used for each column.

We finally loop through all of the WidgetPlacements specific for the column and use the widgettemplate ViewComponent to render each widget.

The last step is creating the widget template ViewComponent.

Finishing with the Widget Template

The widget template is the last piece for making Tuxboard more modular. Again, it's the same process as the other two ViewComponents.

  1. Create a WidgetTemplate folder under Shared\Components
  2. Add a Default.cshtml and a WidgetTemplateViewComponent class file.

Shared\Components\WidgetTemplate\Default.cshtml

@model Tuxboard.Core.Domain.Entities.WidgetPlacement

@if (Model.UseTemplate)
{
    <!-- Widgets -->
    <div class="card @(Model.Collapsed ? "collapsed" : string.Empty)"
         data-id="Model.WidgetPlacementId">

        <div class="card-header">
            @Model.GetSettingValueByName("widgettitle")
        </div>

        <div class="card-body">
            @await Component.InvokeAsync(Model.Widget.Name, Model)
        </div>

    </div>
}
else
{
    @await Component.InvokeAsync(Model.Widget.Name, Model)
}

Shared\Components\WidgetTemplate\WidgetTemplateViewComponent.cs

[ViewComponent(Name="widgettemplate")]
public class WidgetTemplateViewComponent : ViewComponent
{
    public IViewComponentResult Invoke(WidgetPlacement placement)
    {
        return View(placement);
    }
}

I feel we need to explain the Widget template a little bit.

The condition at the top (if (Model.UseTemplate)) is what wraps the widget with a header and border around it. UseTemplate is a property on the WidgetPlacement letting the ViewComponent know whether it's a static widget or not.

Look at it this way: If it doesn't use a template (the else), it will render the widget ViewComponent without borders making it a static widget on the dashboard. The whole idea is for each widget to have a handle or grip (a.k.a the header) so a user can move widgets around the dashboard. If there isn't a header, there won't be options for the widget (which we'll add later and cover in a future post, but this is the ViewComponent where we'll do it).

When you run the application, it should look exactly as it was before. The only difference is it's a little more modular so we can work with each individual part of the dashboard.

Why Did We Do This?

There are a couple of reasons we separated everything:

  1. All of our widgets will have a consistent UI in our dashboard.
  2. In the future, we'll incorporate Refresh features into the Tuxbar for refreshing the dashboard and provide a way to refresh individual widgets through the widget toolbar. For the long term, it's easier to request a single widget ViewComponent than a full dashboard and parsing HTML to find a widget.
  3. We're more confident in adding features to an isolated ViewComponent without breaking something else.

When we made this dashboard more modular, it throws us into the "pit of success" for additional features.

Conclusion

In this post, we broke apart a dashboard into something more modular. Now that we've finished breaking out the ViewComponents, we can look at moving widgets on the dashboard.

What's Next?

Full examples located at Tuxboard.Examples.