In this 4th part of the series about ASP.NET Core and Sircl we will see how easy it is to support Bootstrap modals. We will change a multi-page web application to use Bootstrap modals with almost no changes and without javascript.
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:
In this 4th part of the series about ASP.NET Core and Sircl we will see how easy it is to support Bootstrap modals. Bootstrap is one of the most popular CSS framework for developing (responsive) websites. It also comes standard with the ASP.NET and ASP.NET Core project templates.
The Contact Manager
But first things first. If we want to add a modal to a web application, we first must have an application... Before doing anything with Sircl, we will create a simple “contact manager” application where we can add and edit contacts.
The Contact Manager app contains 2 pages: a list of contacts and a details page. The purpose being to demonstrate Bootstrap modals, I didn’t bother putting a database underneath: de data is stored in a static variable.
Here is what de controller looks like:
public class HomeController : Controller
{
[HttpGet]
public IActionResult Index()
{
var model = new IndexModel()
{
Items = DataContext.Contacts.Values
.OrderBy(c => c.FirstName).ThenBy(c => c.LastName)
.ToList()
};
return View(model);
}
[HttpGet]
public IActionResult New()
{
var model = new EditModel()
{
Item = new Contact() { Id= DataContext.Contacts.Values.Max(c => c.Id) + 1 }
};
return EditView(model);
}
[HttpGet]
public IActionResult Edit(int id)
{
var model = new EditModel() { Item = DataContext.Contacts[id] };
return EditView(model);
}
[HttpPost]
public IActionResult Save(EditModel model)
{
if (ModelState.IsValid)
{
DataContext.Contacts[model.Item.Id] = model.Item;
return RedirectToAction("Index");
}
return EditView(model);
}
[NonAction]
private IActionResult EditView(EditModel model)
{
model.Countries = ISO3166.Country.List
.OrderBy(c => c.Name)
.Select(c => new SelectListItem(c.Name, c.TwoLetterCode));
return View("Edit", model);
}
}
It’s fairly straightforward. The Index method returns a page with all contacts ordered by name. No paging, filtering, searching or different sorting supported.
The New method can be invoked to create a new contact. It creates an EditModel for a new contact and returns the Edit view.
The Edit method also returns the Edit view, but for a given (existing) contact.
The Save method gets the EditModel. If the model is valid, it saves the contact and redirects to the Index action.
The EditView method is a helper method to make sure the list of countries is loaded in the model before rendering the Edit view. To get a list of countries, I used the ISO3166 nuget from Jørn Schou-Rode (https://www.nuget.org/packages/ISO3166/).
The Index view is pretty straightforward too:
@model IndexModel
<h1>Contacts</h1>
<a asp-action="New">Create a new contact</a>
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Id</td>
<td><a asp-action="Edit" asp-route-id="@item.Id">@item.FirstName</a></td>
<td><a asp-action="Edit" asp-route-id="@item.Id">@item.LastName</a></td>
<td><a asp-action="Edit" asp-route-id="@item.Id">@item.Email</a></td>
</tr>
}
</tbody>
</table>
The Index page renders a table with a row per contact where most cells link to the Edit action.
The Edit view, used to create new or edit existing contacts, is a bit longer due to the various fields and Bootstrap styling:
@model EditModel
<form asp-action="Save" method="post">
<input asp-for="Item.Id" type="hidden"/>
<h1>Contact</h1>
<div asp-validation-summary="All" class="alert alert-danger mb-3">
<strong>Following errors have occured:</strong>
</div>
<div class="mb-3">
<label asp-for="Item.FirstName" class="form-label">First name:</label>
<input asp-for="Item.FirstName" class="form-control">
<span asp-validation-for="Item.FirstName"></span>
</div>
<div class="mb-3">
<label asp-for="Item.LastName" class="form-label">Last name:</label>
<input asp-for="Item.LastName" class="form-control">
<span asp-validation-for="Item.LastName"></span>
</div>
<div class="mb-3">
<label asp-for="Item.Email" class="form-label">Email:</label>
<input asp-for="Item.Email" class="form-control">
<span asp-validation-for="Item.Email"></span>
</div>
<div class="mb-3">
<label asp-for="Item.CountryCode" class="form-label">Country:</label>
<select asp-for="Item.CountryCode" asp-items="Model.Countries" class="form-control">
@if (Model.Item.CountryCode == null) { <option></option> }
</select>
<span asp-validation-for="Item.CountryCode"></span>
</div>
<button type="submit" class="btn btn-primary">Save</button>
</form>
Nothing special here again: a form that submits to the Save action using the post method. A hidden field for the contact Id, fields and labels for the other contact properties, and a submit button.
I have also added a validation summary on top.
That’s it for the Contact Manager. If you want to see the Contact class definition or other code parts, check out the source code in annex to this article.
Or better, download the code and run it. The code with this article is what we have build so far, without the Bootstrap modal: the before situation!
Changing to use a modal
So far the application has 2 pages and the browser navigates forth and back between them while we add and edit contacts.
Suppose we would want to stay on the Index page and have the Edit page rendered in a dialog, in a Bootstrap modal. Staying on the Index page, the user would be more focussed and managing contacts would feel less “disruptive”.
So let’s go for it!
Adding Sircl Support
As mentioned, Bootstrap support is included out of the box with the ASP.NET templates. We still need to add Sircl support though. For that, add the following line in the _Layout.cshtml template, add the end of the head section:
<link href="https://cdn.jsdelivr.net/npm/sircl@2.4.4/sircl-bundled.min.css" rel="stylesheet" />
At the end of the body, add the following lines:
<script src="https://cdn.jsdelivr.net/npm/sircl@2.4.4/sircl-bundled.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sircl@2.4.4/sircl-bootstrap5.min.js"></script>
(The Sircl javascript references must be placed after including jQuery. For the rest, you organize the files the way you want. It’s a best practice to have the styling on top and the scripts at the bottom but it is not a requirement.)
Notice that we have included 2 scripts: the Sircl bundle, and the Sircl Bootstrap 5 extension. The latter provides support for Bootstrap components such as modals, collapses and tabs.
If you download the code, you’ll notice I already added the references to Sircl in the _Layout.cshtml file for you.
Adding a Modal
We can now add the modal. You can copy sample code from the Bootstrap site and adapt, or you can copy this code:
<div class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Contact</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div id="contactmodal" class="modal-body target">
(Content comes here)
</div>
</div>
</div>
</div>
This code represents a Bootstrap modal. We’ve already set the title to “Contact” and removed the content of the modal-body element.
What is more important is what we have added: we have added an id “contactmodal” to the modal-body
element, and we have also added the “target” class to it. Later more about these.
This code has to be appended to the Index.cshtml file (or the _Layout.cshtml file) as it must be present when the user is on the index page.
Not a Bootstrap fan ?
Instead of using a Bootstrap modal, you can also use native HTML5 dialogs. Simply replace the above modal code by this single element:
<dialog id="contactmodal" class="dialog-modal target"></dialog>
Other modals or dialogs are currently not supported, but Sircl is extensible and you can add support yourself. You can even contribute to the Sircl project. I’ll be glad to help!
Linking to the Modal
Next, we need to specify that clicking on a contact should open the edit form inside the modal.
With Sircl, this is really easy: we simply declare the modal-body
element as the target of our hyperlinks, as in:
<a href="/Home/Edit/1" target="#contactmodal">Gerald</a>
The target of the hyperlink contains a CSS selector to the element in which the response of this link should be written. It can be any valid CSS selector (including a relative CSS selector), so instead of using an id you could also have written target=".modal .target"
, but if you define multiple modals it may get confusing.
While Sircl does the magic of intercepting the call and making it an Ajax call to retrieve content for the #contactmodal
element, it is the Bootstrap extension to Sircl that does the magic of showing the modal as soon as it receives content. Whenever Sircl loads new content in a Bootstrap modal, the Sircl Bootstrap extension will show this modal.
The complete Index view is now:
@model IndexModel
<h1>Contacts</h1>
<a asp-action="New" target="#contactmodal">Create a new contact</a>
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Id</td>
<td><a asp-action="Edit" asp-route-id="@item.Id" target="#contactmodal">@item.FirstName</a></td>
<td><a asp-action="Edit" asp-route-id="@item.Id" target="#contactmodal">@item.LastName</a></td>
<td><a asp-action="Edit" asp-route-id="@item.Id" target="#contactmodal">@item.Email</a></td>
</tr>
}
</tbody>
</table>
<div class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Contact</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div id="contactmodal" class="modal-body target">
(Content comes here)
</div>
</div>
</div>
</div>
That’s it for the views. No changes are required in the Edit view.
Server-side handling
And to our controller we need only very few changes.
First of all, in the EditView method, we should return a PartialView instead of a View, as we do not want a full page (including headers and footers and navigation menu) to be rendered inside the modal. By returning a PartialView we only render the view itself without the layout template, which is exactly what we want.
In part 2 of this series we have also seen that you could also do that in a more generic way in the _Layout.cshtml.
So the EditView method now becomes:
[NonAction]
private IActionResult EditView(EditModel model)
{
model.Countries = ISO3166.Country.List
.OrderBy(c => c.Name)
.Select(c => new SelectListItem(c.Name, c.TwoLetterCode));
return PartialView("Edit", model);
}
Then, when saving the contact, we don’t want to redirect to the Index page since we already are on that page. Instead we simply want to close the modal. And this can be done by returning NoContent().
If we changed or created a contact, we also want the Index page to reload. We can ask Sircl to reload the page by returning a response header asking so.
The full Save method now has become:
[HttpPost]
public IActionResult Save(EditModel model)
{
if (ModelState.IsValid)
{
DataContext.Contacts[model.Item.Id] = model.Item;
Response.Headers["X-Sircl-History"] = "reload";
return NoContent();
}
return EditView(model);
}
All other methods of the controller remain the same.
Notice that when the Save fails (ModelState
is not valid), the Edit view is rendered again (probably with validation messages). The response of the Save action is then still rendered inside the modal-body
because that element was marked as target by means of the target
class.
Conclusion
As demonstrated by this example, transforming a multi-page web application into a single page application with modals is as easy as a breeze with Sircl.
We basically just set the target of our links to refer to the modal to render the links in, and made the controller correctly respond to the new situation...
In the next article we will cover another use-case of modals (or dialogs) in web applications: the case of a modal used inside a form to edit part of the form data.
In the mean time, check out further Bootstrap support features on:
https://www.getsircl.com/Doc/v2/Bootstrap