Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web

Build Rich Web Apps with ASP.NET Core and Sircl – Part 2

5.00/5 (2 votes)
10 Nov 2023CPOL12 min read 9.7K   46  
In this series, we will see how to build interactive web applications in ASP.NET Core with the help of Sircl.
In the previous article, we learned about Event-Actions as a way to declare client-side behaviour in our web pages. Today, we will learn about the other keystone of Sircl: partial page loading.

Introduction

In this series, we will see how to build great interactive web applications – applications that typically require extensive amounts of JavaScript code or are written in JavaScript frameworks – easily with only ASP.NET Core and Sircl.

Sircl is an open-source client-side library that extends HTML to provide partial updating and common behaviours and makes it easy to write rich applications relying on server-side rendering.

In each part of this series, we will cover a “programming problem” typical to rich web applications using server-side technology, and see how we can solve this problem in ASP.NET Core using Sircl.

In the previous article, we learned about Event-Actions as a way to declare client-side behaviour in our web pages. Today, we will learn about the other keystone of Sircl: partial page loading.

 

Dynamic Behaviours

Dynamic Behaviours as we have seen in the previous part, using Event-Actions, is a powerful concept that can handle simple yet frequent interactions.

For instance, a set of checkboxes where the second only makes sense if the first one is selected. Take an option to have a video conference, and the option to record it. To record it, it must be a video conference:

HTML
<label>
   <input type="checkbox" name="videoconf" ifunchecked-uncheck="input[name='record']">
   Video conference
</label>
<label>
   <input type="checkbox" name="record" ifchecked-check="input[name='videoconf']">
   Record the conference
</label>

You can create little components based on Event-Actions, for instance, a nullable Yes/No field:

HTML
<label>
   <input type="radio" name="MyBoolean" value="True"> Yes
</label>
<label>
   <input type="radio" name="MyBoolean" value="False"> No
</label>
<span onclick-uncheck="input[name='MyBoolean']">&times;</span>

You can turn this into an ASP.NET Editor Template for nullable Booleans for instance. But more on writing editors and components with Sircl later.

Sometimes however, client-side Event-Actions are not sufficient. There are two reasons why Event-Actions might be inadequate:

  1. The interaction is too complex. Event-Actions can for instance not manage complex “and” / “or” situations.
  2. A (web)service or database needs to be queried to determine how to update the UI.

Take, for instance, an international address entry form where the user needs to choose country and – if applicable – state. Whether or not a state must be chosen depends on the country (for US and Canada it is required to indicate a state, but that is not the case for most other countries), and of course, the list of states to choose from, depends also from the chosen country:

Quote:

As long as no country is chosen, the system does not know whether a state is required or not, and which states the user should choose from...

I have seen solutions using JavaScript where all possible states of all countries were hardcoded in the page and downloaded to the client. That is obviously not an ideal solution and will not be workable in all situations. Sometimes the amount of data is just too large, or you do not want to expose it. Or the server needs to take decisions combining complex server-side logic and data.

So let’s see how we can fix this problem with Sircl.

Re-rendering Forms with Sircl

The following could be the Razor view of the address entry form shown above (styling elements removed for readability):

ASP.NET
@model AddressFormModel

<form method="post" asp-action="Next">
    <fieldset>
        <legend>Address</legend>
        <div>
            <label>Name: *</label>
            <input type="text" asp-for="Address.Name">
        </div>
        <div>
            <label>Street &amp; Number: *</label>
            <input type="text" asp-for="Address.Street">
        </div>
        <div>
            <label>City: *</label>
            <input type="text" asp-for="Address.City">
        </div>
        <div>
            <label>Country: *</label>
            <select asp-for="Address.CountryCode" asp-items="Model.Countries">
                <option value="">(select a country)</option>
            </select>
        </div>
        @if (Model.States.Any())
        {
            <div>
                <label>State: *</label>
                <select asp-for="Address.StateCode" asp-items="Model.States">
                    <option value="">(select a state)</option>
                </select>
            </div>
        }
    </fieldset>

    <button type="submit">Next</button>

</form>

The StateCode field is only to be shown for countries that have states, hence the Razor IF instruction. But the problem is the user can change the country on the form. We then need to “re-render” the form, to execute the Razor IF statement. For that we need to post back the form to the server: we need to submit the form.

To submit the form automatically when the country is changed, we can use the onchange-submit Event-Action, which is just a class to add on the CountryCode SELECT element:

ASP.NET
<select class="onchange-submit" asp-for="Address.CountryCode" asp-items="Model.Countries">
    <option value="">(select a country)</option>
</select>

This will submit the form with the default action, the action defined on the FORM element. So we’ll have to change the default action to “Index” (or whatever action initially rendered the form) and move the “Next” action to the “Next” button, as in:

ASP.NET
<form method="post" asp-action="Index">
    ...
    <button type="submit" asp-action="Next">Next</button>
</form>

It’s a less usual design, but if you think of it, it makes sense: let the form render itself and let the buttons define which action they take. If you have different buttons for different actions, it feels natural to add the action on the button itself.

The asp-action attribute translates to an action attribute when placed on a FORM element, and translates to a formaction attribute when placed on a button. The formaction attribute is a standard HTML attribute that allows a submit element to override the default submit action of a form. Similarly, the HTML standard defines formenctype, formmethod, formnovalidate and formtarget attributes to override the form’s defaults.

We now have a working version: the user can enter an address. And whenever the user changes the country, the form is posted and re-rendered with the proper state list. And apart from switching the action URLS on the form and the button, we only had to add an onchange-submit Event-Action!

This is the controller code for the Index action:

C#
public IActionResult Index(AddressFormModel model)
{
    model.Countries = DataStore.Countries
        .Select(c => new SelectListItem(c.Name, c.Code, 
                c.Code == model.Address.CountryCode));
    model.States = DataStore.States
        .Where(s => s.CountryCode == model.Address.CountryCode)
        .Select(s => new SelectListItem(s.Name, s.Code, 
                s.Code == model.Address.StateCode));
            
    return View(model);
}

The full code of this first solution can be downloaded here:

But this solution is not perfect: whenever the country is changed, the form is submitted and therefore a full page load is performed by the browser. This also makes the form unusable from keyboard only. Let’s see if we can do better.

Introducing Page Part Requests

Sircl offers the ability to request the server to update part of a page (or part of a form). We can use this here to update the list of states to choose from using a call to the server.

What Sircl basically offers, is a way to perform an Ajax call and have that Ajax call return HTML code to replace (or extend) a part of the current page. Essentially allowing us to create a simple Single Page Application or SPA.

Such an Ajax page part request is created automatically by Sircl whenever a URL is to be retrieved - or a form is posted - and the link or form has a “local target”.

Let’s see a regular hyperlink:

HTML
<a href="/SomePage">Go to some page</a>

Nothing special here: clicking the link will let the browser navigate to the page and show a new page.

Now let’s add a target:

HTML
<a href="/SomePage" target="_blank">Go to some page</a>

Same here, the browser will navigate to the page and render a new page. On a new tab.
But when the target value is a CSS selector to a location in the page, then something different happens:

HTML
<a href="/SomePage" target="#here">Go to some page</a>
<div id="here"></div>

In this case, Sircl will intercept the hyperlink, will issue a page part request to load /SomePage using Ajax and will place the response HTML inside the #here DIV element. In short: it will replace part of the page.

Quote:

In short: it will replace part of the page.

And the same applies to forms:

HTML
<form class="target" action="/SomeAction" method="post">
   Name: <input type="text" name="username" />
  <button type="submit">OK</button>
</form>

Here, the target attribute was replaced by a target class. Adding the target class is the same as placing a target attribute with a CSS selector that matches the current element (the FORM element here). Submitting the form will in this case replace the content of the FORM element with the response from the server.

Server-side, the handling of a page part request only requires you to return the view as PartialView or to set the _Layout property to null so that the layout template is not returned.

More on partial page loading with Sircl on:
https://www.getsircl.com/Doc/v2/PartialLoading

Re-rendering with Page Part Requests

So let’s update our address entry form to use page part requests. There are several ways to do so with Sircl, let’s see two options:

Option 1 – Only Update the States List

In this solution, we will update only the list of states, and only when the country is changed.

So we need an action method (I am using MVC here) we can invoke to get just the list of states (to get the whole select control I mean, not just the content of the list, as whether the select control is there or not also depends on the chosen country):

C#
[HttpPost]
public IActionResult StateList(AddressFormModel model)
{
    model.States = DataStore.States
        .Where(s => s.CountryCode == model.Address.CountryCode)
        .Select(s => new SelectListItem(s.Name, s.Code, s.Code == model.Address.StateCode));

    return PartialView(model);
}

The StateList action has the same model parameter as other form handling actions as the whole form will be submitted to it. This ensures the StateList action has access to all the data in the form to take its decision.

This StateList action puts the list of states for the current country in the model’s States property and – if applicable – ensures the current state is selected.

Then the StateList action returns the view, but important, it returns it as a PartialView ! Over the wire, we only want to return the select control, not a whole page with header, navigation bar and footer.

To create the “StateList.cshtml” view, move the StateCode field including the Razor IF statement to a separate view:

ASP.NET
@model AddressFormModel

@if (Model.States.Any())
{
    <div>
        <label>State: *</label>
        <select asp-for="Address.StateCode" asp-items="Model.States">
            <option value="">(select a state)</option>
        </select>
    </div>
}

In the original “Index.cshtml” view, we remove the StateCode field (including its surrounding IF) but replace it by a call to the StateList view. After all, it could be we edit an address already filled in that has a state set. Here, we have included the call to the partial view in a DIV element with id StateListContainer. That is because we will need to refer to it:

ASP.NET
<div id="StateListContainer">
    <partial name="StateList" />
</div>

Finally, we need to define that when the country changes, we want to update the states list. We do this by having the onchange-submit class on the countries control, override the formaction and specify a formtarget attribute pointing to the element to contain the state list control:

ASP.NET
<select class="onchange-submit" asp-for="Address.CountryCode" asp-items="Model.Countries"
        formaction="@Url.Action("StateList")" formtarget="#StateListContainer">
    <option value="">(select a country)</option>
</select>

When the country is changed, the onchange-submit will trigger a form submission. And because the country control is the trigger, Sircl will honour the formaction and formtarget attributes of the country control. So it will post the form to the StateList action and place the returned HTML inside the element with StateListContainer id.

Note that the support for formaction and formtarget (and other form* attributes) on “any” HTML element is a Sircl extension. As in Sircl any element can trigger a form submission, any element can have form* attributes. However, they will only be honoured when Sircl handles the form submission, that is when an Ajax call is made. Full page calls like in our first solution are handled by the browser which supports those attributes only on submit elements.

The full code of this second solution can be downloaded here:

Option 2 – Update the Whole Form

A different approach to the problem is to, when the form needs to be re-rendered, let the whole form be re-rendered (not just the StateCode control), but just the form (not the whole page).

The advantage of this approach is that we do not need to split the view and have multiple action methods.

To apply this solution, the CountryCode control still needs to have the onchange-submit class, as a change of its value is still the trigger to submit the form. It also has a formtarget, but that should now be pointing to the FORM element. It does not need to have a formaction attribute, as it does not need to overwrite the form’s default action:

ASP.NET
<select class="onchange-submit" asp-for="Address.CountryCode" asp-items="Model.Countries"
        formtarget="#AddressForm">
    <option value="">(select a country)</option>
</select>

And so the form element must get id="AddressForm".

With this, changing the country will submit the whole form to the Index action using an Ajax call.
A ‘normal’ call to the Index action, as when navigating to the address entry form, should return the whole page (the form but also the footer, header, navigation bars, etc). The Ajax call made by Sircl however, should only return the form. So server-side the distinction needs to be made.

Whenever Sircl initiates an Ajax call, it sets the X-Sircl-Request-Type request header to “Partial”.
Instead of just returning the view, the Index action will now have to decide whether to return the view as a regular view (surrounded by the layout template) or as a partial view:

C#
if (Request.Headers["X-Sircl-Request-Type"] == "Partial")
    return PartialView(model);
else
    return View(model);

Alternatively, you can add the following code on top of the _Layout.cshtml template:

ASP.NET
@if (this.Context.Request.Headers["X-Sircl-Request-Type"] == "Partial")
{
    @:@RenderBody()
    return;
}

If you put this in the _Layout.cshtml template, then you can always return View(model); from the controller method, as is it is the surrounding layout template that will decide whether to render or not. Or leave the _Layout.cshtml as is, but change the code in _ViewStart.cshtml to set the Layout variable to null for Sircl Ajax calls.

There is one issue though with this solution: since the whole form is replaced, including the control that has the focus, the focus is lost. This is annoying especially for users using the keyboard for data entry.

Luckily, there’s a little fix for this: we’ll use a sub-target! The sub-target attribute is a Sircl extension that allows to specify which parts of the target to replace. So the target is still the whole form, and the server will still return the HTML code of the whole form, but only the sub-target elements will be replaced.

The sub-target attribute can refer to a class with multiple matches so that multiple elements can be updated without updating the whole form.

By surrounding the StateCode control (including its Razor IF instruction) with an identifiable element and set a sub-target attribute pointing to that element on the CountryCode control (the trigger element), we can keep our server-side code simple, yet only replace the StateCode control and by that keep the focus in place.

The whole address entry form code is now (without styling elements):

ASP.NET
@model AddressFormModel

<form id="AddressForm" method="post" asp-action="Index">
    <fieldset>
        <legend>Address</legend>
        <div>
            <label>Name: *</label>
            <input type="text" asp-for="Address.Name">
        </div>
        <div>
            <label>Street &amp; Number: *</label>
            <input type="text" asp-for="Address.Street">
        </div>
        <div>
            <label>City: *</label>
            <input type="text" asp-for="Address.City">
        </div>
        <div>
            <label>Country: *</label>
            <select class="onchange-submit" asp-for="Address.CountryCode" asp-items="Model.Countries"
                        formtarget="#AddressForm" sub-target="#StateListContainer">
                <option value="">(select a country)</option>
            </select>
        </div>
        <div id="StateListContainer">
            @if (Model.States.Any())
            {
                <div>
                    <label>State: *</label>
                    <select asp-for="Address.StateCode" asp-items="Model.States">
                        <option value="">(select a state)</option>
                    </select>
                </div>
            }
        </div>
    </fieldset>

    <button type="submit" asp-action="Next">Next</button>

</form>

The Index controller action is:

C#
public IActionResult Index(AddressFormModel model)
{
    // Load data for Countries and States list:
    model.Countries = DataStore.Countries
        .Select(c => new SelectListItem(c.Name, c.Code, 
                c.Code == model.Address.CountryCode));
    model.States = DataStore.States
        .Where(s => s.CountryCode == model.Address.CountryCode)
        .Select(s => new SelectListItem(s.Name, s.Code, 
                s.Code == model.Address.StateCode));

    // Return full view or partial view:
    if (Request.Headers["X-Sircl-Request-Type"] == "Partial")
        return PartialView(model);
    else
        return View(model);
}

(As you can see, the controller code is not aware of the usage of sub-targets.)

We end-up doing the same as in option 1: updating only the states list. But this time, we do not need to create a second controller action method for it, nor do we need to split our Razor view. The impact on our original code is minimal.

The full code of this third solution can be downloaded here:

Conclusion

Again, we have seen how Sircl allows us to write dynamic applications – this time using server-side rendering – with minimal impact on the code by adding declarative HTML attributes and classes instead of writing imperative JavaScript code.

Though in this article, we have taken a simple example, in real world, many complex forms can be implemented this way. Next time, we will go further on this and see how we can manage lists of items using a form and Sircl.

In the mean time, find all about Sircl on:
https://www.getsircl.com/

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)