Hotwired ASP.NET Core Web Application — Part 4

ipek
7 min readJun 18, 2022

--

Hotwired ASP.NET Core Web Application (6 Part Series)

Hotwired ASP.NET Core Web Application — Part 1 (Intro)
Hotwired ASP.NET Core Web Application — Part 2 (NPM, Webpack Setup)
Hotwired ASP.NET Core Web Application — Part 3 (Turbo Drive & Frame)
Hotwired ASP.NET Core Web Application — Part 4 (Turbo Stream)
Hotwired ASP.NET Core Web Application — Part 5 (Stimulus)
Hotwired ASP.NET Core Web Application — Part 6 (Quote Editor)

In part 3, we have used turbo frames to replace the content of a turbo frame or to replace the whole page. But what if we needed more fine-tuning on what to do with the DOM elements such as removing or appending them instead of just replace? This is where turbo streams come into the picture.

Turbo Stream

To see why we need turbo streams and understand how they work, let’s create a simple notes list and implement its CRUD actions. As in the implementation of our previous messages, I didn’t want to use any permanent storage for the notes, not to lose our focus on Hotwire technologies. Here is the simple Note class and singleton Notes class which is populated with two notes initially.

Index.cshtml page with the code showing notes list
_NoteView.cshtml partial view
_NoteAddEdit.cshtml partial view
Notes list with the corresponding HTML

Our new notes list is rendered having three different turbo frame types:

  1. notes_0 turbo frame is a placeholder to show the _NotesAddEdit.cshtml partial view when the Add button is clicked which is therefore initially empty.
  2. notes turbo frame contains each note which will be used to append/prepend our newly created notes.
  3. note_{id} turbo frames are there to work on each note independently.

Let’s start with the “create” action. When we click the Add link which targets the note_0 turbo frame, the OnGetAddNote method is called. In this method, we create a new empty Note instance and return the _NoteAddEdit partial view containing an input to change the note Name, also a Cancel link, and a Save button inside a form.

OnGetAddNote method
Returned response when “Add” button is clicked
Notes list with new note entry editor visible

As we have seen many times, while working on messages, the response replaces the initially empty notes_0 turbo frame with the contents of the response which also contains a turbo frame with the same id. So, far we didn’t do anything new. But now, when we want to save our new note, we want to do two things:

  1. Hide the new note editor at the top
  2. Add our new note to our notes turbo frame

Hiding the new note editor can be accomplished by returning an empty turbo frame with id note_0. But how can we add the new note without returning all the notes? And this is where the turbo stream comes to the rescue. When we click the Save button, the OnPostSaveNote method will be called which will create the new note and at the end return the _NoteAdd partial view, with the new note being the model of this view.

OnPostSaveNote method
_NoteAdd.cshtml partial view

Turbo Streams deliver page changes as fragments of HTML wrapped in self-executing <turbo-stream> elements.

Each <turbo-stream> element has an action and target attribute that specifies what should happen to the HTML inside the turbo stream.

In the above, _NoteAdd partial view contains two turbo-stream elements:

  1. The first turbo-stream element has the target attribute set to note_0 and the action attribute set to replace. This means, it will replace the DOM element with id note_0, with the empty content to hide the editor.
  2. And the second one has the action="append" target="notes" attributes that instruct to append the content inside the turbo stream to the DOM element with id notes.

As we see in the code, turbo-stream elements must wrap their content inside a <template> element and also we can render more than one <turbo-stream> element inside a single response. And the targeted element does not have to be a turbo frame; it can be any HTML element containing the corresponding id attribute.

What about this line that we set before returning the partial view?Response.ContentType = "text/vnd.turbo-stream.html";

When a form with a POST, PUT, PATCH, DELETE method is submitted, Turbo automatically injects text/vnd.turbo-stream.html into the set of response formats in the request’s accept header. So, Turbo is signaling that it can accept responses of this MIME type. If we click a link, for example, we will not see this MIME type in the request’s accept header.

POST request accepting turbo stream with “text/vnd.turbo-stream.html" entry in the accept header

When Turbo is ready for this content type, we also need to set this MIME type on our response. Because only Turbo applies the turbo stream updates instead of the normal merging logic, if the content type for the response is text/vnd.turbo-stream.html. You can comment out the above line and try adding a new note. You will see that it will not work.

Now that we have covered how turbo streams work, let’s move on to the “Edit” action which is almost the same as the “Add”. When we edit a note, we call the OnGetEditNote action, which finds the Note instance with the given id and again returns _NotesAddEdit partial view with the found Note as the model of the view. And to save the update, we again call OnPostSaveNote, which updates the Note instance and returns the _NoteEdit partial view.

This partial view also returns a turbo stream response to replace the edited Note instance with its view (_NotesView).

And finally, the “Delete” action calls the OnPostDeleteNote method, which removes the Note instance from the list and returns the _NoteDelete.cshtml partial view.

And _NoteDelete view contains one turbo stream element that targets the deleted element (note_{deletedId}) and action remove which basically removes that element from the page.

Before moving on to the next section, you can download and run the code covering this part from here.

Delivering Turbo Streams over WebSockets

Turbo stream elements can also be delivered by the server over a WebSocket to bring real-time updates made by other users or processes. And to implement this in our project, we need to add some external code.

First of all, we need to render our partial views to a string to deliver them through WebSockets. For this, I have used the RazorPartialToStringRenderer service that I have found on the “Learn Razor Pages” website. I won’t get into the details of the code, but it basically locates the given partial view, renders its output to a StringWriter, and returns the content of the writer as a string.

Secondly, we need to add SignalR to our project. SignalR uses the WebSocket transport if available and falls back to older transports when necessary. We first create an AppHub class that inherits from the SignalR.Hub class, that will handle client-server communication. In our case, we only want to push our server-side HTML to clients, so we do not need to add any methods.

Now, we need to register the RazorPartialToStringRenderer service and configure the SignalR server in our Program.cs file.

Program.cs with the code to register new service and SignalR

Finally, I used scottiemc7’s signalRSourceElement.ts code in hotwire-aspnet-demo-chat project to create the signalr-turbo-stream HTML element that will listen to the AppHub for turbo stream responses. I have renamed it and saved it under the ClientApp\js folder with the name signalRTurboStreamElement.js.

For this element to work, we also need to install the SignalR package from the terminal while being inside the ClientApp folder:

npm install @microsoft/signalr --save

And we also import this file into our app.js file:

import './signalRTurboStreamElement'

And now, it’s time to use these new services to deliver our turbo stream responses over the WebSocket. We will first change the constructor of IndexModel for the injection of the two new services.

And let’s use these services inside the OnPostSaveNote and OnPostDeleteNote methods, where we were returning turbo stream results:

OnPostSaveNot and OnPostDeleteNote using WebSockets to deliver turbo streams

In the above code, we have commented out the parts that set the ContentType and return PartialViewResults. Instead, we used our RazorPartialToStringRenderer service to render our partial views to a string, renderedViewStr. Then we used the SignalR hub service to deliver this string to all clients with the “NotesChanged” method.

Lastly, we need to add our custom HTML element to the Index.cshtml to connect to our AppHub and listen for the “NoteChanged” messages.

And that’s it. Now we can first check if our CRUD actions are working as before. Then, we can also test the real-time updates delivered to each client by opening two browser windows and adding, editing, and deleting notes which I have shown in the following video.

Summary

We have covered and used the three main components of Turbo:
1. Turbo Drive
2. Turbo Frame
3. Turbo Stream

However, when compared to the Rails applications, in which Turbo and Stimulus are integrated and are the default front-end framework, it is easier and more robust than our implementation in an ASP.NET application. For example, when using turbo frames, it is very important to name them with good conventions. Unless we pay attention to this, as the application grows, we might find ourselves in a naming hell. In Rails, there is a dom_id helper which converts an object into a unique id. We can and should implement the same helper in our application which will name the turbo frames with the agreed-upon naming conventions in one place.

Also, while using turbo streams, either delivering them over HTTP or WebSockets, we can refactor our response implementations into a class to make our application more resilient against the possible changes in the Turbo library.

And the other downside of our implementation is that it is tightly coupled to turbo streams: however, as advised in turbo streams documentation, we should build a “progressively enhanced” version of our pages. But we have used partial views in all our responses instead of full-page responses.

In part 5, I will explain the Stimulus and show how we can use it in our application. You can download the code covering this part from here.

References

[1] Rendering A Partial View To A String
[2] Introduction to SignalR
[3] Tutorial: Get started with ASP.NET Core SignalR

--

--