/
3. Add front-end, render agenda, set up front-end models.md
781 lines (633 loc) · 29.1 KB
/
3. Add front-end, render agenda, set up front-end models.md
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
## Building the Front End
In this session, we'll add the front end web site, with a public (anonymous) home page showing the conference agenda.
## Add a FrontEnd project
We'll start by creating the new front end project for the web site.
### Adding the FrontEnd Project using Visual Studio
1. If using Visual Studio, right-click on the Solution and select **Add** > **New Project...**.
1. Select the *ASP.NET Core Web App* template. Name the project *FrontEnd* and press **OK**.
1. Select *ASP.NET Core 6.0 (LTS)* from the drop-down list in the top-left corner.
1. Leave all the other default values and clidk the **Create** button.
1. Right-click on the *FrontEnd* project and select **Add** > **Project Reference**, then add a reference to the *ConferenceDTO* project.
### Adding the FrontEnd Project via the Command Line
1. Open a command prompt and navigate to the root `ConferencePlanner` directory.
1. Run the following command:
```bash
dotnet new webapp -o FrontEnd
```
1. Next we'll need to add a reference to the ConferenceDTO project from the new FrontEnd project. From the command line, navigate to the FrontEnd project directory and execute the following command:
```bash
dotnet add reference ../ConferenceDTO/ConferenceDTO.csproj
```
## Create and wire-up an API service client
Our *FrontEnd* project has a reference to *ConferenceDTO*, but not *BackEnd*. The *FrontEnd* will communicate with the *BackEnd* using the HTTP endpoints. We'll create a common class to talk to our backend web API service using .NET's `HttpClient`.
### Create the API service client class
1. Create a folder called *Services* in the root of the *FrontEnd* project.
1. In this folder, add a new interface called `IApiClient` with the following members:
``` csharp
using ConferenceDTO;
namespace FrontEnd.Services;
public interface IApiClient
{
Task<List<SessionResponse>> GetSessionsAsync();
Task<SessionResponse?> GetSessionAsync(int id);
Task<List<SpeakerResponse>> GetSpeakersAsync();
Task<SpeakerResponse?> GetSpeakerAsync(int id);
Task PutSessionAsync(Session session);
Task<bool> AddAttendeeAsync(Attendee attendee);
Task<AttendeeResponse?> GetAttendeeAsync(string name);
Task DeleteSessionAsync(int id);
}
```
1. Staying in this folder, add a new class called `ApiClient` that implements the `IApiClient` interface by using `HttpClient` to call out to our BackEnd API application and JSON serialize/deserialize the payloads:
``` csharp
using System.Net;
using ConferenceDTO;
namespace FrontEnd.Services;
public class ApiClient : IApiClient
{
private readonly HttpClient _httpClient;
public ApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<bool> AddAttendeeAsync(Attendee attendee)
{
var response = await _httpClient.PostAsJsonAsync($"/api/Attendee", attendee);
if (response.StatusCode == HttpStatusCode.Conflict)
{
return false;
}
response.EnsureSuccessStatusCode();
return true;
}
public async Task<AttendeeResponse?> GetAttendeeAsync(string name)
{
if (string.IsNullOrEmpty(name))
{
return null;
}
var response = await _httpClient.GetAsync($"/api/Attendee/{name}");
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<AttendeeResponse>();
}
public async Task<SessionResponse?> GetSessionAsync(int id)
{
var response = await _httpClient.GetAsync($"/api/Session/{id}");
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SessionResponse>();
}
public async Task<List<SessionResponse>> GetSessionsAsync()
{
var response = await _httpClient.GetAsync("/api/Session");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<SessionResponse>>() ?? new();
}
public async Task DeleteSessionAsync(int id)
{
var response = await _httpClient.DeleteAsync($"/api/Session/{id}");
if (response.StatusCode == HttpStatusCode.NotFound)
{
return;
}
response.EnsureSuccessStatusCode();
}
public async Task<SpeakerResponse?> GetSpeakerAsync(int id)
{
var response = await _httpClient.GetAsync($"/api/Speaker/{id}");
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SpeakerResponse>();
}
public async Task<List<SpeakerResponse>> GetSpeakersAsync()
{
var response = await _httpClient.GetAsync("/api/Speaker");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<SpeakerResponse>>() ?? new();
}
public async Task PutSessionAsync(Session session)
{
var response = await _httpClient.PutAsJsonAsync($"/api/Session/{session.Id}", session);
response.EnsureSuccessStatusCode();
}
}
```
### Configure the API client
1. Open the *Program.cs* file
1. Locate the line which reads `var app = builder.Build();` and add the following code above it:
``` csharp
builder.Services.AddHttpClient<IApiClient, ApiClient>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["serviceUrl"]);
});
```
> This adds an instance of `HttpClientFactory` with its base URL pulled from the application configuration, which will point to our BackEnd API application.
1. Add a using statement for `FrontEnd.Services` to `Program.cs`.
1. Find the URL for your BackEnd API in the `BackEnd/Properties/launchSettings.json` file. It will be on a line that lists both an *http* and *https* URL - you want the *https* one. By default, you won't be running on IIS Express, so you don't need that URL.
1. Open the `appsettings.json` file in your *FrontEnd* project and add the configuration key for `serviceUrl` pointing to the URL your specific BackEnd API application is configured to run in. The result should look like this:
``` json
{
"ServiceUrl": "https://localhost:7112",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
```
ASP.NET Core has a rich configuration system. Often, simple configuration variables are stored in `appsettings.json` in development and overwritten by environment varibles or other configuration providers in production.
> **Warning**
> You should never store secrets like passwords, tokens, or database connection strings in `appsettings.json`. Secrets should be stored securely and outside of your application source code directory, for instance in the [Secret Manager](https://docs.microsoft.com/aspnet/core/security/app-secrets#secret-manager).
## List the sessions on the home page
Now that we have an API client we can use to talk to our BackEnd API application, we'll update the home page to show a basic list of all sessions for the conference to ensure the *FrontEnd* can talk to the *BackEnd* correctly.
### Load the data into the PageModel
The home page of the site will display our conference sessions. In ASP.NET Core Razor Pages, the default page is `Index.cshtml`. Razor Pages uses a combination of a view template file (e.g. `Index.cshtml`) and a paired `PageModel` class (e.g. `Index.cshtml.cs`). The `PageModel` class allows separation of the logic of a page from its presentation. It defines page handlers for requests sent to the page and the data used to render the page.
The `OnGet` method executes when the page is accessed via HTTP Get (as opposed to other HTTP verbs, like HTTP Post). This method completes before the view (`Index.cshtml`) is rendered, so its job is to prepare all the dynamic content the page will display.
To start with, our `OnGet` method's job will be to load all the sessions using the `ApiClient`. For our display purposes, we want to allow browsing through the conference sessions grouped by day and ordered by start time. We'll do that using standard LINQ methods.
1. Open the `/Pages/Index.cshtml.cs` file
1. Edit the constructor to accept the `IApiClient` interface and store it in a local field:
``` csharp
protected readonly IApiClient _apiClient;
public IndexModel(ILogger<IndexModel> logger, IApiClient apiClient)
{
_logger = logger;
_apiClient = apiClient;
}
```
1. Add some properties to the `IndexModel` class to store sessions and other data we'll need when rendering the page (adding a `using ConferenceDTO;` statement to resolve the compiler error):
``` csharp
public IEnumerable<IGrouping<DateTimeOffset?, SessionResponse>> Sessions { get; set; } = null!;
public IEnumerable<(int Offset, DayOfWeek? DayofWeek)> DayOffsets { get; set; } = null!;
public int CurrentDayOffset { get; set; }
```
1. Add a page handler method to handle GET requests to the page, that loads the session data and calculates the data required to build the day navigation UI:
``` csharp
public async Task OnGet(int day = 0)
{
CurrentDayOffset = day;
var sessions = await _apiClient.GetSessionsAsync();
var startDate = sessions.Min(s => s.StartTime?.Date);
DayOffsets = sessions.Select(s => s.StartTime?.Date)
.Distinct()
.OrderBy(d => d)
.Select(day => ((int)Math.Floor((day!.Value - startDate)?.TotalDays ?? 0),
day?.DayOfWeek))
.ToList();
var filterDate = startDate?.AddDays(day);
Sessions = sessions.Where(s => s.StartTime?.Date == filterDate)
.OrderBy(s => s.TrackId)
.GroupBy(s => s.StartTime)
.OrderBy(g => g.Key);
}
```
### Render the sessions list on the home page
Now that we have loaded the sessions into the `PageModel` class properties, we can render them in the page.
1. Open the `/Pages/Index.cshtml` Razor Page file
1. Replace the `<div>` containing the welcome message with the following Razor markup to show the sessions as a simple list, grouped by time-slot:
``` html
<div class="agenda">
<h1>My Conference @System.DateTime.Now.Year</h1>
@foreach (var timeSlot in Model.Sessions)
{
<h4>@timeSlot.Key?.ToString("HH:mm")</h4>
<ul>
@foreach (var session in timeSlot)
{
<li>@session.Title</li>
}
</ul>
}
</div>
```
1. Right-click the solution, select **Properties** and set both *BackEnd* and *FrontEnd* as startup projects.
1. Run the *FrontEnd* application at this stage and we should see the sessions listed on the home page
> Creating multiple startup projects in VS Code can be done by updating the `launch.json` file with the *compounds* part. Here is an example `launch.json` file which will run both projects and display the Swagger page for the *BackEnd* project.
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "BackEnd",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/BackEnd/bin/Debug/net6.0/BackEnd.dll",
"args": [],
"cwd": "${workspaceFolder}/BackEnd",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)",
"uriFormat": "%s/swagger"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": "FrontEnd",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/FrontEnd/bin/Debug/net6.0/FrontEnd.dll",
"args": [],
"cwd": "${workspaceFolder}/FrontEnd",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
],
"compounds": [
{
"name": "FrontEnd/BackEnd",
"configurations": [
"FrontEnd",
"BackEnd"
]
}
]
}
```
### Add buttons to allow showing sessions for different days
The `OnGet` method takes a parameter to select a day of the conference to display. Let's add buttons to allow the user to show sessions for the different days of the conference.
1. In `/Pages/Index.cshtml`, add the following markup , below the `<h1>` we added previously:
``` html
<ul class="nav nav-pills mb-3">
@foreach (var day in Model.DayOffsets)
{
<li role="presentation" class="nav-item">
<a class="nav-link @(Model.CurrentDayOffset == day.Offset ? "active" : null)" asp-route-day="@day.Offset">@day.DayofWeek?.ToString()</a>
</li>
}
</ul>
```
We're using *Bootstrap* attributes like `nav` and `mb-3` to style the content.
What's interesting here is the navigation element. The `asp-route-day` attribute is a [tag helper](https://docs.microsoft.com/aspnet/core/mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-6.0#asp-route-value) which passes the route value in the URL to the page, where it is processed by the `OnGet` method's filter logic.
As each button is rendered, the logic determines if it is the active day and sets the `active` attribute, causing *Bootstrap* to style it as such.
1. Run the application again and try clicking the buttons to show sessions for the different days.
## Update the sessions list UI
1. Make the list of sessions better looking by updating the markup to use [Bootstrap cards](https://getbootstrap.com/docs/5.1/components/card/):
``` html
<h4>@timeSlot.Key?.ToString("HH:mm")</h4>
<div class="row">
@foreach (var session in timeSlot)
{
<div class="col-md-3 mb-4">
<div class="card shadow session h-100">
<div class="card-header">@session.Track?.Name</div>
<div class="card-body">
<h5 class="card-title"><a asp-page="Session" asp-route-id="@session.Id">@session.Title</a></h5>
</div>
<div class="card-footer">
<ul class="list-inline mb-0">
@foreach (var speaker in session.Speakers)
{
<li class="list-inline-item">
<a asp-page="Speaker" asp-route-id="@speaker.Id">@speaker.Name</a>
</li>
}
</ul>
</div>
</div>
</div>
}
</div>
```
1. Run the page again and see the updated sessions list UI. Click the buttons again to show sessions for the different days.
> **Note**
> The sessions and speakers appear as links, but those links aren't hooked up yet. We'll implement those next.
## Add a session details page
Now that we have a home page showing all the sessions, we'll create a page to show all the details of a specific session.
### Add a Session Razor Page
1. Add a new Razor Page using the *Razor Page - Empty* template. Call the page 'Session.cshtml' and save it in the */Pages* directory.
1. Accept the `IApiClient` in the constructor and add supporting members to the Page model `SessionModel`. The class should look like this:
``` csharp
using ConferenceDTO;
using FrontEnd.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace FrontEnd.Pages;
public class SessionModel : PageModel
{
private readonly IApiClient _apiClient;
public SessionResponse? Session { get; set; }
public int? DayOffset { get; set; }
public SessionModel(IApiClient apiClient)
{
_apiClient = apiClient;
}
}
```
1. Add a page handler method to retrieve the Session details and set them on the model:
``` csharp
public async Task<IActionResult> OnGet(int id)
{
Session = await _apiClient.GetSessionAsync(id);
if (Session == null)
{
return RedirectToPage("/Index");
}
var allSessions = await _apiClient.GetSessionsAsync();
var startDate = allSessions.Min(s => s.StartTime?.Date);
DayOffset = Session.StartTime?.Subtract(startDate ?? DateTimeOffset.MinValue).Days;
return Page();
}
```
This `OnGetAsync` method attempts to load the session by `Id`. If it fails, we redirect back to the main page with the session list. If it succeeds, we perform an additional request and do some quick date math to determine which day of the conference the session is on.
1. Open the `Session.cshtml` file and add markup to display the details and navigation UI:
``` html
@page "{id}"
@model SessionModel
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-page="/Index">Agenda</a></li>
<li class="breadcrumb-item"><a asp-page="/Index" asp-route-day="@Model.DayOffset">Day @(Model.DayOffset + 1)</a></li>
<li class="breadcrumb-item active">@Model.Session!.Title</li>
</ol>
<h1>@Model.Session.Title</h1>
<span class="label label-default">@Model.Session.Track?.Name</span>
@foreach (var speaker in Model.Session.Speakers)
{
<em><a asp-page="Speaker" asp-route-id="@speaker.Id">@speaker.Name</a></em>
}
@foreach (var para in Model.Session!.Abstract!.Split("\r\n", StringSplitOptions.RemoveEmptyEntries))
{
<p>@para</p>
}
```
## Add a page to show speaker details
We'll next add a page to show details for a given speaker.
1. Add a new Razor Page using the *Razor Page - Empty* template. Call the page `Speaker.cshtml` and save it in the */Pages* directory.
1. Accept the `IApiClient` in the constructor and add supporting members to the Page model `SpeakerModel`:
``` csharp
public class SpeakerModel : PageModel
{
private readonly IApiClient _apiClient;
public SpeakerResponse Speaker { get; set; }
public SpeakerModel(IApiClient apiClient)
{
_apiClient = apiClient;
}
}
```
1. Add a page handler method to retrieve the Speaker details and set them on the model:
``` csharp
public async Task<IActionResult> OnGet(int id)
{
Speaker = await _apiClient.GetSpeakerAsync(id);
if (Speaker == null)
{
return NotFound();
}
return Page();
}
```
1. Open the *Speaker.cshtml* file and add markup to display the details and navigation UI:
``` html
@page "{id}"
@model SpeakerModel
<ol class="breadcrumb">
<li class="breadcrumb-item"><a asp-page="/Speakers">Speakers</a></li>
<li class="breadcrumb-item active">@Model.Speaker!.Name</li>
</ol>
<h2>@Model.Speaker.Name</h2>
<p>@Model.Speaker.Bio</p>
<h3>Sessions</h3>
<div class="row">
<div class="col-md-5">
<ul class="list-group">
@foreach (var session in Model.Speaker.Sessions)
{
<li class="list-group-item">
<a asp-page="Session" asp-route-id="@session.Id">@session.Title</a>
</li>
}
</ul>
</div>
</div>
```
## Add search functionality
We'll add a page to allow users to search the conference agenda, finding sessions and speakers that match the supplied search term. This will require work on every part of our application - the *BackEnd* will process the search, the *FrontEnd* will display it, and both will use new classes in the *ConferenceDTO* library.
### Add DTO for search results
1. Add a new DTO class `SearchResult` in the DTO project:
```csharp
namespace ConferenceDTO;
public record SearchResult
{
public SearchResultType Type { get; set; }
public SessionResponse? Session { get; set; }
public SpeakerResponse? Speaker { get; set; }
}
public enum SearchResultType
{
Session,
Speaker
}
```
### Add a search endpoint
1. Add a `SearchEndpoints` class to the *Endpoints* directory in the *BackEnd* project. It just has one endpoint that accepts a search term and searchs for sessions and speakers with matching titles or names, and concatenates the results as a `List<SearchResult>`:
```csharp
using BackEnd.Data;
using ConferenceDTO;
using Microsoft.EntityFrameworkCore;
namespace BackEnd.Endpoints
{
public static class SearchEndpoints
{
public static void MapSearchEndpoints(this IEndpointRouteBuilder routes)
{
routes.MapGet("/api/Search/{term}", async (string term, ApplicationDbContext db) =>
{
var sessionResults = await db.Sessions.Include(s => s.Track)
.Include(s => s.SessionSpeakers)
.ThenInclude(ss => ss.Speaker)
.Where(s =>
s.Title!.Contains(term) ||
s.Track!.Name!.Contains(term)
)
.ToListAsync();
var speakerResults = await db.Speakers.Include(s => s.SessionSpeakers)
.ThenInclude(ss => ss.Session)
.Where(s =>
s.Name!.Contains(term) ||
s.Bio!.Contains(term) ||
s.WebSite!.Contains(term)
)
.ToListAsync();
var results = sessionResults.Select(s => new SearchResult
{
Type = SearchResultType.Session,
Session = s.MapSessionResponse()
})
.Concat(speakerResults.Select(s => new SearchResult
{
Type = SearchResultType.Speaker,
Speaker = s.MapSpeakerResponse()
}));
return results
is IEnumerable<SearchResult> model
? Results.Ok(model)
: Results.NotFound();
})
.WithTags("Search")
.WithName("GetSearchResults")
.Produces<IEnumerable<SearchResult>>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
}
}
}
```
### Add search methods to the IApiClient
1. Add the `SearchAsync` method to `IApiClient`:
```csharp
Task<List<SearchResult>> SearchAsync(string query);
```
1. Add the implementation to `ApiClient`:
```csharp
public async Task<List<SearchResult>> SearchAsync(string term)
{
var response = await _httpClient.GetAsync($"/api/search/{term}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<SearchResult>>() ?? new();
}
```
### Add a search page to the Front End
1. Add a new Razor Page using the *Razor Page - Empty* template. Call the page `Search.cshtml` and save it in the */Pages* directory.
1. Accept the `IApiClient` in the constructor and add supporting members to the Page model `SearchModel`:
``` csharp
public class SearchModel : PageModel
{
private readonly IApiClient _apiClient;
public string Term { get; set; } = String.Empty;
public List<SearchResult> SearchResults { get; set; } = new();
public SearchModel(IApiClient apiClient)
{
_apiClient = apiClient;
}
}
```
1. Add a page handler method to retrieve the search results and set them on the model, deserializing the individual search items to the relevant model type:
``` csharp
public async Task OnGetAsync(string term)
{
Term = term;
if (!string.IsNullOrWhiteSpace(term))
{
SearchResults = await _apiClient.SearchAsync(term);
}
}
```
1. Open the *Search.cshtml* file and add markup to allow users to enter a search term and display the results, casting each result to the relevant display model type:
``` html
@page
@using ConferenceDTO
@model SearchModel
<div class="search">
<h1>Search</h1>
<form method="get">
<div class="input-group mb-3">
<input asp-for="Term" placeholder="Search for sessions or speakers..." class="form-control" />
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="submit">Go!</button>
</div>
</div>
@if (Model.SearchResults?.Count > 0)
{
<p>
@Model.SearchResults.Count result(s)
</p>
}
</form>
</div>
<div class="row">
@foreach (var result in Model.SearchResults!)
{
<div class="col-md-12">
@switch (result.Type)
{
case SearchResultType.Speaker:
<div class="card shadow mb-3">
<div class="card-header">
<h3 class="card-title">
Speaker:
<a asp-page="Speaker" asp-route-id="@result.Speaker.Id">
@result.Speaker!.Name
</a>
</h3>
</div>
<div class="card-body">
<p>
@foreach (var session in result.Speaker.Sessions)
{
<a asp-page="/Session" asp-route-id="@session.Id">
<em>@session.Title</em>
</a>
}
</p>
<p>
@result.Speaker.Bio
</p>
</div>
</div>
break;
case SearchResultType.Session:
<div class="card shadow mb-3">
<div class="card-header">
<h3 class="card-title">
Session:
<a asp-page="Session" asp-route-id="@result.Session.Id">@result.Session!.Title</a>
</h3>
@foreach (var speaker in result.Session.Speakers)
{
<a asp-page="/Speaker" asp-route-id="@speaker.Id">
<em>@speaker.Name</em>
</a>
}
</div>
<div class="card-body">
<p>
@result.Session.Abstract
</p>
</div>
</div>
break;
}
</div>
}
</div>
```
1. Add the search link to the navigation pane in `Pages/Shared/_Layout.cshtml`:
```html
<li class="nav-item">
<a class="nav-link text-dark" asp-page="/Search">Search</a>
</li>
```
1. Click on the `Search` link to test the new search feature.
**Next**: [Session #4 - Authentication](4.%20Add%20auth%20features.md) | **Previous**: [Session #2 - Back-end](2.%20Build%20out%20BackEnd%20and%20Refactor.md)