8 - ASP.NET
Grid-based listing
Changing between list-based listing and grid-based listing might be beneficial feature in the browsers.
Grid-like listing emphasizes the images and shows only the core information (e.g., name of the product and its price), while list-based listing might contain more textual information with smaller images (or none at all).
To have this feature, we need to change the controller and the view as well.
The controller only tracks the layout while the view renders it as requested.
| ProductsController.cs |
|---|
| // GET: Products
public async Task<IActionResult> Index(string viewType)
{
ViewData["ViewType"] = viewType ?? "list";
return View(await _context.Products.ToListAsync());
}
|
| Index.cshtml |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84 | @model IEnumerable<WebShop.Models.Product>
@{
ViewData["Title"] = "Index";
string viewType = ViewData["ViewType"] as string;
}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Products</h1>
<div>
<a asp-action="Index" asp-route-viewType="list"
class="btn @(viewType == "list" ? "btn-primary" : "btn-outline-primary")">
<i class="bi bi-list-ul"></i> List View
</a>
<a asp-action="Index" asp-route-viewType="grid"
class="btn @(viewType == "grid" ? "btn-primary" : "btn-outline-primary")">
<i class="bi bi-grid-3x3-gap"></i> Grid View
</a>
</div>
</div>
@if (viewType == "grid")
{
<div class="row row-cols-1 row-cols-md-3 g-4">
@foreach (var item in Model)
{
<div class="col">
<div class="card h-100 shadow-sm">
@if (item.Image != null)
{
var base64 = Convert.ToBase64String(item.Image);
var imgSrc = $"data:image/png;base64,{base64}";
<img src="@imgSrc" class="card-img-top" style="height: 200px; object-fit: cover;" alt="@item.Name">
}
else
{
<div class="bg-light text-center py-5">No Image</div>
}
<div class="card-body">
<h5 class="card-title">@item.Name</h5>
<p class="card-text text-success fw-bold">$@item.Price</p>
</div>
<div class="card-footer bg-transparent border-top-0">
<a asp-action="Details" asp-route-id="@item.ID" class="btn btn-sm btn-info">Details</a>
<a asp-action="Edit" asp-route-id="@item.ID" class="btn btn-sm btn-warning">Edit</a>
</div>
</div>
</div>
}
</div>
}
else
{
<table class="table table-striped mt-3">
<thead>
<tr>
<th>@Html.DisplayNameFor(model => model.Name)</th>
<th>@Html.DisplayNameFor(model => model.Price)</th>
<th>Image</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>@Html.DisplayFor(modelItem => item.Name)</td>
<td>@Html.DisplayFor(modelItem => item.Price)</td>
<td>
@if (item.Image != null)
{
var base64 = Convert.ToBase64String(item.Image);
<img src="data:image/png;base64,@base64" style="width: 50px;" />
}
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a>
</td>
</tr>
}
</tbody>
</table>
}
|
The next chapter will give an overview about the ViewData.
Passing data to views
Views have access to a weakly typed (also called loosely typed) collection of data.
It means that you don't explicitly declare the type of data you're using.
You can use the collection of weakly typed data for passing small amounts of data in and out of controllers and views.
ViewData
This collection can be referenced through either the ViewData or ViewBag properties on controllers and views.
The ViewData property is a dictionary of weakly typed objects.
The ViewBag property is a wrapper around ViewData that provides dynamic properties for the underlying ViewData collection.
Key lookups are case-insensitive for both ViewData and ViewBag.
Both are dynamically resolved at runtime.
Since they don't offer compile-time type checking, both are generally more error-prone than using a viewmodel.
For that reason, some developers prefer to minimally or never use ViewData and ViewBag.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | public class Address
{
public string Name { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
}
public IActionResult SomeAction()
{
ViewData["Greeting"] = "Hello";
ViewData["Address"] = new Address()
{
Name = "Steve",
Street = "123 Main St",
City = "Hudson",
State = "OH",
PostalCode = "44236"
};
return View();
}
|
1
2
3
4
5
6
7
8
9
10
11
12 | @{
// Since Address isn't a string, it requires a cast.
var address = ViewData["Address"] as Address;
}
@ViewData["Greeting"] World!
<address>
@address.Name<br>
@address.Street<br>
@address.City, @address.State @address.PostalCode
</address>
|
Another approach that uses the ViewDataAttribute.
Properties on controllers or Razor Page models marked with the [ViewData] attribute have their values stored and loaded from the dictionary.
In the following example, the Home controller contains a Title property marked with [ViewData].
The About method sets the title for the About view:
1
2
3
4
5
6
7
8
9
10
11
12
13 | public class HomeController : Controller
{
[ViewData]
public string Title { get; set; }
public IActionResult About()
{
Title = "About Us";
ViewData["Message"] = "Your application description page.";
return View();
}
}
|
| <head>
<title>@ViewData["Title"] - WebApplication</title>
...
|
ViewBag
ViewBag isn't available by default for use in Razor Pages PageModel classes.
ViewBag is a Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.DynamicViewData object that provides dynamic access to the objects stored in ViewData.
ViewBag can be more convenient to work with, since it doesn't require casting.
The following example shows how to use ViewBag with the same result as using ViewData above:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | public IActionResult SomeAction()
{
ViewBag.Greeting = "Hello";
ViewBag.Address = new Address()
{
Name = "Steve",
Street = "123 Main St",
City = "Hudson",
State = "OH",
PostalCode = "44236"
};
return View();
}
|
| @ViewBag.Greeting World!
<address>
@ViewBag.Address.Name<br>
@ViewBag.Address.Street<br>
@ViewBag.Address.City, @ViewBag.Address.State @ViewBag.Address.PostalCode
</address>
|
Since ViewData and ViewBag refer to the same underlying ViewData collection, you can use both ViewData and ViewBag and mix and match between them when reading and writing values.
Summary of the differences between ViewData and ViewBag:
ViewData
- Derives from
ViewDataDictionary, it has dictionary properties that can be useful, such as ContainsKey, Add, Remove, and Clear,
- Keys in the dictionary are strings, whitespace is allowed. Example:
ViewData["Some Key With Whitespace"],
- Any type other than a string must be cast in the view to use
ViewData.
ViewBag
ViewBag isn't available by default for use in Razor Pages PageModel classes,
- Derives from
Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.DynamicViewData, it allows the creation of dynamic properties using dot notation (@ViewBag.SomeKey = <value or object>), and no casting is required. The syntax of ViewBag makes it quicker to add to controllers and views,
- Simpler to check for null values. Example:
@ViewBag.Person?.Name
When to use ViewData or ViewBag
Both ViewData and ViewBag are equally valid approaches for passing small amounts of data among controllers and views.
The choice of which one to use is based on preference.
You can mix and match ViewData and ViewBag objects, however, the code is easier to read and maintain with one approach used consistently.
Both approaches are dynamically resolved at runtime and thus prone to causing runtime errors.
Some development teams avoid them.
Ordering and filtering
The same technique can be used for implementing the ordering methods: new parameters to the controller and a new form in the view.
| ProductsController.cs |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 | private readonly List<SelectListItem> sortOptions;
public ProductsController(ApplicationDbContext context)
{
// ...
sortOptions =
[
new SelectListItem { Value = "name_asc", Text = "Name (A - Z)" },
new SelectListItem { Value = "name_desc", Text = "Name (Z - A)" },
new SelectListItem { Value = "price_asc", Text = "Price (Low - High)" },
new SelectListItem { Value = "price_desc", Text = "Price (High - Low)" },
];
}
// GET: Products
public async Task<IActionResult> Index(string viewType, string sortOrder)
{
// ...
ViewData["CurrentOrder"] = sortOrder;
ViewBag.SortList = new SelectList(sortOptions, "Value", "Text", sortOrder);
var products = from p in _context.Products select p;
products = sortOrder switch
{
"name_desc" => products.OrderByDescending(x => x.Name),
"price" => products.OrderBy(x => x.Price),
"price_desc" => products.OrderByDescending(x => x.Price),
_ => products.OrderBy(x => x.Name) // default
};
return View(await products.ToListAsync());
}
|
The filtering part is just an addition to the ordering, the same mechanism can be used.
| ProductsController.cs |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | public async Task<IActionResult> Index(string viewType, string sortOrder, string searchText)
{
// ...
ViewData["CurrentFilter"] = searchText;
if (!string.IsNullOrEmpty(searchText))
{
products = products.Where(x => x.Name.ToLower().Contains(searchText.ToLower()));
}
products = sortOrder switch
{
// ...
}
|
| Index.cshtml |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 | // At the beginning of the file after the @{ ... } block.
<div class="btn-group">
<a asp-action="Index"
asp-route-viewType="list"
asp-route-searchString="@ViewData["CurrentFilter"]"
asp-route-sortOrder="@ViewData["CurrentSort"]"
class="btn @(viewType == "list" ? "btn-primary" : "btn-outline-primary")">List</a>
<a asp-action="Index"
asp-route-viewType="grid"
asp-route-searchString="@ViewData["CurrentFilter"]"
asp-route-sortOrder="@ViewData["CurrentSort"]"
class="btn @(viewType == "grid" ? "btn-primary" : "btn-outline-primary")">Grid</a>
</div>
<form asp-action="Index" method="get" class="mb-4">
<div class="row g-3 align-items-center">
<div class="col-auto">
<input type="text" name="searchText" value="@ViewData["CurrentFilter"]"
class="form-control" placeholder="Search by name..." />
</div>
<div class="col-auto">
<select name="sortOrder" asp-items="ViewBag.SortList" class="form-select"></select>
</div>
<input type="hidden" name="viewType" value="@ViewData["ViewType"]" />
<div class="col-auto">
<button type="submit" class="btn btn-primary">Apply</button>
<a asp-action="Index" class="btn btn-outline-secondary">Clear</a>
</div>
</div>
</form>
|
References