Alexander Zeitler

A ASP.NET Razor Fragments / Single View approach - not only for HTMX

Published on Wednesday, January 18, 2023

Photo by Clint Adair on Unsplash

A few months ago, the HTMX team published an essay on so-called template fragments to adhere to the Locality of Behavior (and in my opinion also cohesion) principle in server side rendered views.

If you're using a template engine it is likely you end up with several views like typical parent / child/detail views:

Until now, it has been common that this results in multiple view/component files. However, this comes with a disadvantage:

While separation of concern is improved now, in the same way cohesion and locality of behavior deteriorates:

Locality of Behavior

The behaviour of a unit of code should be as obvious as possible by looking only at that unit of code

Cohesion

Cohesion refers to the degree to which the elements inside a module belong together.

The proposed solution is to have a single template file but being able to address fragments of it by a key. By providing the key by e.g. a controller method calling the view engine, it will just render the fragment.

This is the proposed sample as an HTML file providing template fragment support:

<html>
    <body>
        <div hx-target="this">
          #fragment archive-ui
            #if contact.archived
            <button hx-patch="/contacts/${contact.id}/unarchive">Unarchive</button>
            #else
            <button hx-delete="/contacts/${contact.id}">Archive</button>
            #end
          #end
        </div>
        <h3>Contact</h3>
        <p>${contact.email}</p>
    </body>
</html>

The fragment id here is archive-ui. So you can either render the full view or just two buttons.

When reading this essay after it has been published, it appealed to me a lot, but I didn't think it could be supported by ASP.NET Core Razor without extending the View engine.

So I went over to GitHub and created an issue, requesting for that enhancement. Although it has been triaged and moved to the Backlog this won't be available soon (read: at least not in .NET 8).

After creating the issue, I was repeatedly annoyed by the lack of support, but did not pursue the issue further - until yesterday.

The topic came up again in the dotnet-htmx channel on the HTMX discord server.

Based on the discussion, a rough idea formed in my head how this could be solved without extending Razor at all, getting full IDE support and all the like.

Here's what I came up with - let's just talk code:

First, we define some models:

public class FragmentModel
{
  public FragmentModel(
    string fragmentId
  )
  {
    FragmentId = fragmentId;
  }

  public string FragmentId { get; set; }
}

public class ChildModel : FragmentModel
{
  public int Id { get; }

  public ChildModel(
    int id
  ) : base("Detail")
  {
    Id = id;
  }
}

public class ParentModel : FragmentModel
{
  public List<ChildModel> Childs { get; }

  public ParentModel(
    List<ChildModel> childs
  ) : base("Full")
  {
    Childs = childs;
  }
}

Then we define a single View file:

@model RazorFragments.Models.FragmentModel

@{
  Layout = "_Layout";
}


@{
  void RenderDetail(
    ChildModel child)
  {
    <div class="bg-gray-200">
      @Html.ActionLink(child.Id.ToString(), "Detail", new
      {
        id = child.Id
      })
    </div>
  }

  void RenderFull(
    ParentModel parent)
  {
    <div class="bg-blue-500 p-4">
      @{
        @foreach (var parentChild in parent.Childs)
        {
          RenderDetail(parentChild);
        }
      }
    </div>
  }
}


@{
  switch (Model.FragmentId)
  {
    case "Full":
      RenderFull(Model as ParentModel);
      break;
    case "Detail":
      RenderDetail(Model as ChildModel);
      break;
  }
}

And everything gets tied together in our controller:

public class RazorFragmentController : Controller
{
  // GET
  public IActionResult Index()
  {
    return View(
      new ParentModel(
        new List<ChildModel>()
        {
          new(1),
          new(2)
        }
      )
    );
  }

  [Route("/detail/{id}")]
  public IActionResult Detail(
    [FromRoute] int id
  )
  {
    return View("Index", new ChildModel(id));
  }
}

This is just a first draft stitched together in the middle of the night so I'm curious about your thoughts, feedback and suggestions for improvement.

The full sample can be found on GitHub.

Update (2023/01/26)

I created another sample which is closer to the mockup in from the intro:

List view List view

Details view Details view

What are your thoughts about "A ASP.NET Razor Fragments / Single View approach - not only for HTMX"?
Drop me a line - I'm looking forward to your feedback! email
Imprint | Privacy