Hotwired ASP.NET Core Web Application — Part 6

ipek
10 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)

And finally, we are in the last part, where we will implement what we have learned so far, about Turbo and Stimulus, to develop the quote editor. I advise you to check out the original quote editor to see how it works so that you will better understand what we are going to develop.

First, we have to prepare our data, and here is a simple diagram of our classes. In the application, you can find these class definitions in the Models\Quote.cs file.

Class Diagram

To keep the data, I created a QuoteRepository class and registered it as a singleton service in the Program.cs file. QuoteRepository class populates a static list with predefined entities in its constructor.

builder.Services.AddSingleton<IRepository<Quote>, QuoteRepository>();

The quote editor has two main pages:

  1. Quotes.cshtml: Lists the quotes with their names.
    - We can add\edit\delete a Quote on this page.
  2. QuoteDetail.cshtml: Shows a quote with Date and Item details.
    - We can add\edit\delete a QuoteDate on this page.
    - We can add\edit\delete a QuoteItem on this page.

These two pages are shown in blue in the below diagram:

Quote editor pages

As you noticed, a Quote, QuoteDate, or a QuoteItem have independent CRUD actions which can be executed inline on a single page, thanks to Turbo. But if you remember, in Part 4, I mentioned that my implementation was dependent on turbo streams, but it was advised that we should develop the application even when the turbo was not available. In my quote editor implementation, I decided to follow this suggestion and therefore needed to add three more pages instead of using partial views as before:

  1. Quote.cshtml
  2. QuoteDate.cshtml
  3. QuoteItem.cshtml

When turbo is available, we will use but not navigate to these pages; the navigation will only be between the above mentioned Quotes.cshtml and QuoteDetail.cshtml pages.

Quotes page and adding, editing a quote

Below is the Quotes.cshtml main page code which shows the quotes as a list.

Quotes.cshtml

And below is the simplified markup of the Quotes page which has the “Add Quote” button and three quotes. The three quotes are rendered inside a turbo frame shown in green. The “Add Quote” button is not inside a turbo frame, but it targets the quote_0 turbo frame using a data-turbo-frame attribute.

Rendered Quotes.cshtml page

The quote_0 turbo frame, shown in blue, is rendered as an empty turbo frame which exists for replacement with an editor. But if you check out the code above, if there are no quotes then we render the Quote\_Empty.cshtml partial view inside the quote_0 turbo frame which renders the following view:

Also notice the empty div tag with id notification and the turbo frame with id quotes (surrounding the all the quotes) in yellow. We will soon mention them.

Now, let’s look at the flow of interactions for a Quote instance:

  1. Inside the Quotes.cshtml page, we use the Quote\_View.cshtml partial view to show each quote by rendering its name with a delete and edit button.
Rendered Quote\_View.cshtml partial view

2. When we click the Add Quote button, the OnGetAdd named handler of Quote.cshtml page is called and it returns the Quote.cshtml page. And similarly, if we click the edit button, the OnGetEdit named handler is called and again returns the Quote.cshtml page.

As we can see below, the Quote.cshtml page code renders either the Quote\_View.cshtml or the Quote\_AddEdit.cshtml partial view, depending on the handler route parameter defined at the top with the @page directive.

Quote.cshtml page

4. If you check out these Quote\_View.cshtml and Quote\_AddEdit.cshtml partial views, you will see that they both have the necessary markup surrounded by the turbo frames. So, when turbo is not enabled, if we click the add quote button, we will go to the Quote.cshtml page and the URL will change to Quote/Add/0. But if turbo is enabled, the same full page response will be processed by turbo. And since our add and edit buttons are inside a turbo frame, as we explained in detail in part 3, ONLY content inside a matching <turbo-frame> will be used when the page is updated.

Adding a new quote without and with Turbo

5. After we add or edit a quote and click the save button, OnPostAdd and OnPostEdit handlers are called, respectively. After completing the necessary data operations successfully, we have to return a response. And this time, to support both with and without Turbo cases, we have to return two different responses. But first, we need to be able to detect if the request is accepting turbo stream responses. And if you remember from Part 4, we can check request’s accept header to do that by checking whether it contains "text/vnd.turbo-stream.html" format or not. I have added the following two extension methods on the HttpRequest class inside the Utils\ExtensionMethods.cs file.

We will use the AcceptsTurboStream method to decide our response type:

  • If it returns false, we will redirect to the Quote\View\{id} URL to return Quote.cshtml page with OnGetView named handler. This will return a full page result by rendering the Quote\_View.cshtml partial view.
  • If it returns true, we will broadcast the Quote\_Add.cshtml partial view in OnPostAdd and the Quote\_Edit.cshtml partial view in OnPostEdit.

Let’s analyze what Quote\_Add.cshtml partial view contains.

Quote\_Add.cshtml partial view response

Quote\_Add.cshtml partial view does three things with three turbo-stream custom elements:

  1. The first turbo-stream element has the target attribute set to notification. This means that, it will target the DOM element with id notification and replace its content with the markup inside the template tags. Here the replacement is made with the _SuccessNotification.cshtml partial view which is a common partial view, used after all our successful CRUD operations to show the success message for a few seconds. I will explain in detail how this partial view works, later in this part. Now let’s just focus on the turbo part.
Success notification

Remember that while _Add.cshtml partial is being processed, we know that turbo is included and we are on the Quotes page, doing our add and edit operations with inline editors on this same page. So, for the turbo stream to work, there has to be a DOM element with id notification to replace. And if you remember, we have seen it above, in yellow, on the rendered Quotes.cshtml view.

2. The second turbo-stream element replaces the content of turbo frame quote_0 with empty content which was previously replaced with form elements by _AddEdit.cshtml.

3. And the third turbo-stream element targets the element with id quotes to prepend the newly added quote to the that element. We have mentioned that quotes element above with yellow in the rendered Quotes.cshtml view which was surrounding our all quote instances.

And Quote\_Edit.cshtml is similar to Quote\_Add.cshtml .

It again shows the success message with the first turbo-stream. And the second turbo-stream replaces the content of turbo frame quote_{id} with Quote\View.cshtml partial view which was previously replaced with form elements by _AddEdit.cshtml.

We see that when the turbo is enabled, each turbo-stream element in our partial view responses act like puzzle pieces that will fit a specific place on the current view.

Now, let’s also go over the above OnPostAdd named handler inside the Quote.cshtml.cs code behind file to highlight some other important points.

Quote.cshtml.cs OnPostAdd method

Showing model state errors

We know that OnPostAdd method is called when we save a new quote. So, the first thing this method does is to check if the model state is valid. If it’s not, it returns the same page which is Quote.cshtml with Add handler which will again render the Quote\_AddEdit.cshtml. Inside this partial view, the following markup exists:

<vc:error-summary model=@Model></vc:error-summary>

ErrorSummary is a view component defined under the Pages\Shared\Components folder. It basically renders the model state errors if the error count is more than 0. Below images show this component’s output when we try to post a new quote without a name.

Showing model state errors without and with turbo

Showing the success message

We have mentioned the _SuccessNotification.cshtml partial view before which accepts as a model a JsonMessage instance. In the above OnPostAdd code, we create a JsonMessage instance and assign it to a local variable. If turbo is available, we assign this message to our Message property, declared in our base PageModel class (TutorialPageModel) and decorated with the BindProperty attribute. And while analyzing the turbo stream responses in Quote\_Add.cshtml partial view, we have seen that _SuccessNotification partial view was binding to this Message property.

However, if turbo is not available, we have seen that we are not returning a partial view and instead redirecting to the Quote\View\{id} URL. So, assigning the message to the same Message property will not work in this case. Since HTTP is a stateless protocol, the value assigned to the Message property in OnPostAdd will not be available when OnGetView is called. To solve this problem, we use TempData which is a storage container for data that needs to be available to a separate HTTP request. And on the redirected page, in this case Quote.cshtml, we access the TempData as following:

Normally, TempData only store simple types. To store complex types, we must manually serialize and deserialize the value. And I have done this, by adding two extension methods for ITempDataDictionary interface which were used in the code above.

ITempDataDictionary extension methods in ExtensionMethods.cs

OnPostEdit is very similar to OnPostAdd, so I will not explain it here, and I will move on to examining how we delete a quote.

Deleting a quote

Whether turbo is included or not, we can delete a quote while we are on the Quotes.cshtml main page. So, I have defined the OnPostDelete named handler in the Quotes.cshtml page. But similar to what we did in add and edit actions, our response changes depending on the availability of turbo.

OnPostDelete method in Quotes.cshtml

After removing the Quote from the repository,

  • If turbo is not available, we redirect to the same Quotes.cshtml page with default handler “List” and make a full page reload.
  • And if turbo is available, then we just broadcast the Quote\_Delete.cshtml partial view to make necessary changes with turbo.

Here is the Quote\_Delete.cshtml partial view code:

Similar to add and edit, the first turbo stream is there to show the success message by replacing the DOM element with id notification. The second turbo stream is only rendered if the quote count is zero. If we have deleted the last quote and there aren’t any quotes left in the repository, we target the element with id quote_0 and replace its contents with the Quote\_Empty.cshtml partial view, as we have also used it in the Quotes.cshtml pages. The third turbo stream targets the deleted quote id and removes it.

Showing Quote Details

We have covered all the CRUD actions for a quote instance. If we want to work on the quote details by adding, editing, removing a QuoteDate or QuoteItem, we navigate to the QuoteDetail.cshtml by clicking the link on quote name in Quote\_View.cshtml.

The hyperlink that navigates to QuoteDetail page in Quote\_View.cshtml

Notice that, the anchor tag has the data-turbo-frame attribute set to "_top". We have mentioned _top keyword in part 3. By default, when we navigate within a frame, turbo will target just that frame. But like in this case, we might want to override this behavior. When we navigate from a turbo frame that has target="_top" attribute set or a non-frame element with data-turbo-frame="_top", Turbo will not be looking for a turbo frame with the same id in the response; instead, the response will replace the whole page. So, that’s how we navigate to QuoteDetail page while inside a turbo frame.

I won’t explain the CRUD actions for QuoteDate and QuoteItem. They are very similar to Quote. Also, if you are stuck, you can access the final code from here.

Showing success notification using Stimulus

Our quote editor uses Stimulus only to show the success message. And I used the Stimulus Notification component from the Stimulus Components to do that. First I installed it as following:

npm install — save-dev stimulus-notification

Then I imported and registered it as seen below in our app.js file.

Importing and registering Stimulus Notification component

It’s usage is very easy. We define an HTML element with data-controller attribute set to notification. And also set the necessary data-transition- attributes for animation. data-notification-delay-value attribute is the only attribute that you might want to change. It determines the delay in milliseconds before closing the notification. Inside this element, you define your notification window markup which I have omitted below but you can find in _SuccessNotification.cshtml file.

Summary

Finally, we have implemented our Quote Editor. We even coded it as it would work even when Turbo is not available. I hope this series of articles would help anyone understand and jump start working with Turbo and Stimulus in their ASP.NET Core projects. You can access the final version of project from here.

References

[1] TempData In Razor Pages
[2] Stimulus Components
[3] Tailwind UI

--

--