Creating User-Specific Dashboards

As a final post for this entire series, we'll look at creating dashboards for individual users

Written by Jonathan "JD" Danylko • Last Updated: • Tuxboard •

Collection of people using their smartphones

In the last post, we demonstrated how to use the widget toolbar with capabilities to remove and minimize a widget.

While all of the features we covered in past examples are great, it's only a single dashboard allowing multiple people to add and remove widgets. In real-life, there would be no reason to even create a dashboard like this because multiple users could manipulate that one dashboard causing absolute chaos with "cats and dogs living together...mass hysteria!"

The purpose for building these simple dashboards was to provide examples so fellow developers could see it in action locally.

In today's post, we'll build on what we previously created and give users their own personal dashboard when they log on.

With everything we've done so far, it wouldn't be efficient to type everything in again. The good news is everything is already built for us and we can easily transition our dashboard to a user-based dashboard project.

Let's get started.

Creating the Project

To start, we need to create an ASP.NET Core Web App (Razor Page) by clicking New > Project in Visual Studio.

Screenshot of Create Project in Visual Studio 2022

After clicking Next, enter the specifics of the project.

Screenshot of Configure Project in Visual Studio 2022

For this example, we'll select the "Individual Accounts" option for the AuthenticationType on the "Additional Information" screen.

Screenshot of Additional Information for Project in Visual Studio 2022

Click Create.

The boilerplate gives us a project template which uses MS Identity for logging individuals into the system. The whole reason to use MS Identity is to associate an ID with a user. We could even replace MS Identity with another authentication/authorization library like Azure Entra authentication. The User ID for Tuxboard is a GUID.

If we look at the project, we can see the folder structure contains the Identity Data\Migrations folder with an ApplicationDbContext. Pretty standard.

Next, change the appsettings.json to point to a localhost instance of SQL Server as shown below. Replace "<DatabaseNameHere>" with your database name.

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=localhost;Initial Catalog=<DatabaseNameHere>;Integrated Security=True;MultipleActiveResultSets=True;TrustServerCertificate=True"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Once we have the appsettings.json updated, we can update the database through the Package Manager Console (View > Other Windows... > Package Manager Console).

Update-Database -Context ApplicationDbContext

After hitting enter, our Identity tables should exist in SQL Server.

Adding Tuxboard

Once we have our Identity tables in place, we can add Tuxboard through NuGet to create those tables.

Add Tuxboard to the project through NuGet (Tools > NuGet Package Manager > Manage NuGet Packages for Solution...). The package is called Tuxboard.Core and the version as of now is 1.6.5.

Once Tuxboard is included, we need to add a migration and update the database, but first, we need to update our appsettings.json to include Tuxboard settings. Add the following to the appsettings.json file.

.
.
"TuxboardConfig"
: {   "ConnectionString": "Data Source=localhost;Initial Catalog=<DatabaseNameHere>;Integrated Security=True;MultipleActiveResultSets=True;TrustServerCertificate=True",   "Schema": "dbo" },

After updating the settings, now we can add the migration and update the database. To add the migrations, type the following into the Package Manager Console.

add-migration Initial -Context TuxDbContext -OutputDir Data\Migrations\TuxboardContext

The migrations for Tuxboard were placed in it's own directory below the Data\Migrations folder using the -OutputDir parameter.

Once the migration finishes, we can update the database.

Update-Database -Context TuxDbContext

If the connection string in the appsettings.json is the same as the DefaultConnection, the tables will be added to the database. Based on the appsettings.json, the update-database command will use the TuxboardConfig:ConnectionString to create the schema.

After the tables are created, we can move on to the transitioning code over to the project.

Creating the Dashboard (or not)

At this point, we have two options: we can create a dashboard from scratch or reuse one of our previous examples and copy the code over.

If building a dashboard from scratch, begin building your dashboard adding the features you want and jump down to the next section for code changes for users.

This section will explain all of the moving pieces to copy from one project to another to confirm the dashboard works as expected.

For this project, we'll use the same files from the 08-Widget-Toolbar project since it's a culmination of everything we've worked on so far.

  1. Copy the following files/folders over:
    1. wwwroot/scss (CSS folder)
    2. wwwroot/src (TS/JS folder)
    3. wwwroot/tsconfig.json (file)
    4. /Dto (C# folder)
    5. /Extensions (C# folder)
    6. /Helpers (C# folder)
    7. /Models (C# folder)
    8. /Pages/Shared/Components (C# folder)
  2. Copy /gulpfile.js and /package.json to the root of your project
    1. Right-click on the package.json file and Restore Packages to make sure the gulpfile.js will run during the build process.
  3. For all of the C# folders above, change to the project's namespace instead of 08-Widget-Toolbar.
  4. In the Pages/_viewImports.cshtml, confirm TagHelpers are registered based on your project name.

    @addTagHelper *, <your-project-name-here>
  5. In the Program.cs, add your tuxboard configuration and DbContext

    // Tuxboard Configuration
    var appConfig = new TuxboardConfig();
    builder.Configuration
        .GetSection(nameof(TuxboardConfig))
        .Bind(appConfig);
    
    // Tuxboard DbContext
    builder.Services.AddDbContext<TuxDbContext>(options => {     options.UseSqlServer(appConfig.ConnectionString,         x => x.MigrationsAssembly("09-User-Dashboard")); });
  6. Also in the Program.cs, add the dependency injection as well

    // For Dependency Injection
    builder.Services.AddTransient<IDashboardService, DashboardService>();
    builder.Services.AddTransient<ITuxDbContext, TuxDbContext>();
  7. Update the Index.cshtml and Index.cshtml.cs file with code from the 08-Widget-Toolbar project.
  8. Update the _Layout.cshtml to include Bootstrap and FontAwesome CSS and JavaScript files.

    <link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/lib/fontawesome/css/all.min.css" />
    .
    .
    <script src="~/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/dashboard.min.js"></script>


After all of these changes are copied over from the previous project, running the project will present us with the generic dashboard.

Now that we have a dashboard and MS Identity both in one project, we need to connect the two.

    Getting Personal[-ized]

    The great part about using Identity (or Entra) is the ability to know who a user is when they sign in. To tie everything together, we need a way to identify the user and retrieve their dashboard.

    First, in the Index.cshtml.cs file, we create the GetIdentity() method to retrieve the user ID.

    private Guid GetIdentity()
    {
        var claim = User?.FindFirst(ClaimTypes.NameIdentifier);
        return claim != null 
            ? new Guid(claim.Value) 
            : Guid.Empty;
    }
    

    The GetIdentity() method will be used exclusively throughout each each method.

    If a user is logged in, we locate the ID by finding the NameIdentifier claim and returning it's value.

    If there isn't a logged in user, we simply return an empty Guid.

    With this simple method in place, we can now identify the user and retrieve their personal dashboard.

    The OnGet() method in the Index.cshtml.cs becomes extremely simple now.

    public async Task OnGet()
    {
        var id = GetIdentity();
        if (id != Guid.Empty)
        {
            Dashboard = await _service.GetDashboardForAsync(_config, id);
        }
    }
    

    If we don't know who the user is, don't retrieve a dashboard and dashboard will be null.

    Throughout all of the Index dashboard code, we can now replace:

    var dashboard = await _service.GetDashboardAsync(_config);
    

    with this.

    var id = GetIdentity();
    var dashboard = await _service.GetDashboardForAsync(_config, id);
    

    Essentially, all we're doing In the Index.cshtml.cs file is replacing all GetDashboardAsync(_config) with GetDashboardForAsync(_config, id).

    Now, for every user who logs in, they'll have their own dashboard.

    You Get a Dashboard, You Get a Dashboard...

    Throughout these examples, we make a call to retrieve a dashboard.

    At first, we were using GetDashboardAsync() and for those who aren't asynchronous yet, there is also a synchronous call called GetDashboard().

    Now we're switching gears and using GetDashboardForAsync() (and GetDashboardFor() for synchronous calls).

    Here's the process when a GetDashboardXxxx() method is called.

    If GetDashboardAsync() or GetDashboard() is called, these aren't user-specific and it will load a dashboard from the database where the user is null. If there isn't a dashboard in the database at the time, it will create a brand new one with defaults (which we'll get to in a future post).

    If GetDashboardForAsync() or GetDashboardFor() is called with a user ID, the same thing will occur: it will create a brand new dashboard if one doesn't exist and assign a user ID to it. If there is an existing dashboard for this user, it'll load it.

    Basically, every GetDashboardXxxx() call will always return a dashboard.

    With that said, I return you to your regularly scheduled (dashboard) programming...

    Displaying the Dashboard

    When a user isn't logged in, we shouldn't display a dashboard. Without a dashboard's data, we'll break some of our ViewComponents.

    The two ViewComponents we need to update are the Tuxbar and the Tuxboard Template.

    Remember in our code how the Dashboard property is set to null at the top of our index.cshtml.cs?

    public Dashboard Dashboard { get; set; } = null!;
    

    Let's add another property called HasDashboard.

    public bool HasDashboard => Dashboard != null;
    

    This provides an easy way to identify whether a user has a dashboard or not. If they are logged in, they should have a dashboard (based on our previous section).

    In the Index.cshtml, we can use this new property to add a heading for users not logged in.

    <h3 class="text-center" condition="!Model.HasDashboard">Register or login to view your dashboard.</h3> 
    

    But what about our ViewComponents? What if there isn't a dashboard model for our Tuxbar or TuxboardTemplate ViewComponent?

    We can't really apply a condition TagHelper inside another TagHelper (a drawback in my eyes), but we can examine the model passed into each ViewComponent. Let's look at the TuxboardTemplateViewComponent first.

    [ViewComponent(Name = "tuxboardtemplate")]
    public class TuxboardTemplateViewComponent : ViewComponent
    {
        public IViewComponentResult Invoke(Dashboard model)
        {
            return model == null
                ? Content(string.Empty)
                : View(model);
        }
    }
    

    Since we pass the Dashboard property into the ViewComponent, we can determine whether a user is logged in or not.

    If they aren't logged in (model is null), no content is returned. If they are, they'll see their dashboard.

    Oh...and they'll see their Tuxbar if they're using one. We'll need to apply the same logic to our Tuxbar ViewComponent so it has the same return result.

    [ViewComponent(Name = "tuxbar")]
    public class TuxbarViewComponent : ViewComponent
    {
        public IViewComponentResult Invoke(Dashboard model)
        {
            return model == null 
                ? Content(string.Empty) 
                : View(model);
        }
    }
    

    The empty content is also a way to visually show if something is wrong if a dashboard model isn't passed into the ViewComponent.

    The Final Result

    With everything in place, we can now register users and each user can have their own dashboard.

    Screenshot of Loading a user dashboard on login

    Conclusion

    All of these core examples in the past have led us up to this moment where users can create their own dashboards. While some of these features/examples are bells and whistles, a majority of these example provide a collection of fundamental stepping stones into how users are empowered to create their own workspaces and examine data in various ways. They want to personalize their dashboard based on how they work.

    Today's post covered how to create those user-specific dashboards giving users the freedom to experiment with their layouts.

    Everything from this point forward will be considered extras, features, and supporting posts to expand and enhance Tuxboard dashboards even further.

    ASP.NET 8 Best Practices on Amazon

    ASP.NET 8 Best Practices by Jonathan Danylko


    Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

    Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

    Order now on Amazon.com button

    Picture of Jonathan "JD" Danylko

    Jonathan "JD" Danylko is an author, web architect, and entrepreneur who's been programming for over 30 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

    He currently works at Insight Enterprises as an Architect.

    When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

    comments powered by Disqus