
In this Lecture we will
Learn about the instructor for this course ... Charlie Chiarelli
Retired Highschool Computer Science Teacher with over 35 years experience
Online instructor for the past 10 + years
Udemy instructor for the past 8 years
6th course on Udemy
Other course that are C# based
Beginners C#
Intermediate C#
Developing Games in C#
Other courses
Beginners Javascript
Advanced Excel including macros and VBA programming
Learn about the Aim of the Course
What you will learn ( ASP.NET for true beginners) ... Webforms, MVC,Razor Pages and Blazor
Highlight some Helpful Skills to have
o Beginners knowledge of C#
o Basic understanding of how a web page is created with HTML
o Some knowledge of CSS will help but is not required
Discuss what you need to succeed… a philosophy
Highlight some of the ways this course will help you succeed
o Each lecture starts with a list of objectives/speaking notes
o Every example covered in the lecture is available for download in the resources section … including the objectives/speaking notes
o Almost every lecture has a set of Practice problems with full solutions provided
o The instructor is available for help … replying most times within a day
In this Lecture we will
Take a look at the Big picture ... The Evolution of Web Development
Basic HTML
HTML Forms ... introduction of tags for textboxes and buttons
Server Side (PHP ... Hypertext Preprocessor /ASP.NET ... stands for Active Server Pages)
Client Side Programming (Javascript)
Static vs Dynamic Content
Static Content
Typically HTML (.htm) files return the same thing each time requested
Updates to content require updating the html file
Dynamic content
ASP.NET Web Forms (.aspx) can contain dynamically generated content
No need to update .aspx files for new content
all code runs on the web server the final result is sent back to the client as ordinary HTML
Content typically drawn from other sources like a database
Learn what does ASP.NET do for us?
ASP.NET is the Microsoft platform for developing Web Applications. ASP.NET is part of the larger Microsoft .NET framework. C#, is another component.
It allows us to create dynamic websites using server web controls and applications.
Dynamic websites allow users to query databases in real time rather than loading static pages on each interaction
Using ASP.NET you can create e-commerce sites (like Amazon.com) , data driven portals and just about anything else you can find on the internet.
Learn there are a number of flavors of ASP.NET, for example, Web Forms (Web Sites and Web Applications), Model-View-Controller (MVC) and the newest one .NET Core. This course is aimed at anyone who wants to create dynamic websites with ASP.NET Web Forms. Best of all, you don't need to paste together a jumble of HTML and Script code. Instead you can create full scale web apps using nothing but code (C#) and a design tool like Visual Studio.
MVC(Model View Controller) and .NET Core offer different ways to build dynamic web pages. To some MVC and Core are cleaner and more suited to the web. To others it's a whole lot of extra effort with no clear payoff. Either way, you need a strong knowledge of basic ASP.NET before moving onto the newer technologies.
In this Lecture we will
Highlight the software you need to participate in the course
Visual Studio 2022 Community Edition
7zip
In this Lecture we will
Learn about some of the concepts and applications we will create .. a sampling of web applications
In this Lecture we will
In this Lecture we will
We acquaint ourselves with the Visual Studio IDE by creating a simple ASP.NET Web Application IntroToVSandASPNETwebapps
choose empty website ... add folders and core references for Web Forms
this creates a nearly empty site with a stripped down web.config
you fill in pieces as needed... manually adding a web form (default.aspx)
best starting point because you won't have any extra unnecessary generated files
add an image folder (in solution explorer pane)... then copy image files directly there.
design and source view
Learn to add text to the web page and format using Headings (H1...H6)
note: if you center text all objects center, put in <p> to center only that specific text
Learn to add images to a web page using a number of methods
drag and drop
HTML <IMG> control
Learn to edit in source view with Intellisense
add <br />
uses XHTML rules
Learn to title pages
in properties
choose <DOCUMENT> then modify title
Learn to view page in browser
In this Lecture we will
Discuss the differences between HTML and ASP.NET controls
HTML loads faster ... better for static pages
ASP.NET controls must constantly interact with server
HTML button control is coded in Javascript
when you double click on it, you are put into HTML source onClick event
you then must provide the code for the corresponding function eg. alert("Welcome");
ASP.NET button control is coded in C# ... when you double click on it creates Event Handler
code behind file ... aspx.cs
Create a simple ASP.NET Web application that uses ASP.NET and HTML controls and incorporates C# coding (enter a name and display a greeting) MyFirstWebApp
using labels,text boxes and button controls
add HTML elements .... <HR> from the HTML controls in toolbox
format text eg. font, color (CSS based) SimpleFormatting
uses classes (internal style sheet)
converts text to hyperlink (Format menu)
adds an ASP.NET image control
Visual Studio has a built-in viewer for ASP.NET pages you create... BUT
You can't just open a dynamic page on your own computer (without VS) before it is posted online as you can a regular HTML page.
You must view a dynamic page through a Web Server that has an appropriate application server running.
ASP.NET is the server model we use. It runs in conjunction with Microsoft IIS Web Server .. http://localhost
Discuss the difference between server side code and client side code (view source in browser)
Default.aspx (server-side) ... runat="server" tells Webserver that ASP.NET tags should be processed before sending page to the browser
Default.aspx(client-side) ... browser gets HTML generated by ASP.NET
In this Lecture we will
In this Lecture we will
Reinforce the idea that a Web Application can contain HTML and ASP.NET server controls
Learn to use a number of new controls including
radiobuttons
use the property group name for related radiobuttons
checkboxes
dropdownlists
set autopostback to true
image control
ImageUrl property ... /images/name.jpg
Create several programs to illustrate the concepts above
MoreControls
TeamMemberBios
Challenge you to create your "own" TeamMemberBios site
In this Lecture we will
Recap the use of RadioButtons and C# If Statements Conditionals
Learn how to use the checkboxList ... note the use of <br /> html tag
Simple example picking pizza toppings (pre-cursor to Practice Problem)
foreach(ListItem x in CheckboxList1.Items)
if (x.Selected==true)
Label1.Text+="You picked " + x.Value + "<br />";
ConditionalsUpdated
Practice what we have learned by creating the "Pizza Store" Web Application
PizzaStore
PizzaStoreUpdated (complex if statement)
In this Lecture we will
Look at the concept of field validation
required field validator
range validator
regular expression validator
need to add lines below to web.config for validation controls to work in newest VS Editions
<appSettings>
<add key="ValidationSettings:UnobtrusiveValidationMode" value="None"/>
</appSettings>
Create a program to illustrate the concepts above
ValidatedCustomerForm
In this Lecture we will
Acquaint or reacquaint ourselves with the concept of methods and why they are used
A method is a code block that contains a series of statements. A program causes the statements to be executed by calling the method and specifying any required method arguments
Methods can make programs/web applications more efficient by centralizing similar code blocks in one block and can be re-used wherever necessary
Look at the two types of methods
void type
types that return values
Introduce the use of the C# TryParse built-in methods
Create several programs to illustrate the use of methods in web applications
HelpMethod
PostalCalculator
In this Lecture we will
Take a close look at the calendar control and how to implement it in a web application
get dates
set dates
determine number of days between dates
restrict dates
Create several programs to illustrate these calendar concepts
Calendar (get and set dates)
DaysBetweenDates
TheCalendar (restricts dates ... no weekends/allows multiple date selections)
In this Lecture we will
Discuss the Post Back problem ... Demo with ThePostBackProblem
PostBack is the name given to the process of submitting an ASP.NET page to the server for processing.
every post back reloads web page .... re-executes Page_Load (this is where we could run into problems)
to check for post back use the command IsPostBack
in Page_Load ... add
if (Page.IsPostBack==false) ... checks to see if page loaded for first time or not
Create a program to illustrate the use of PostBack
CheckBoxTest
Offer you the challenge to create a ASP.NET Web Application highlighting your favorite musical band/sports team
which displays a message on opening
on subsequent requests band/team member info is displayed
TeamMemberBiosPostBack
In this Lecture we will
Review and extend our techniques to move between web pages (NavBetweenPages/QueryStringExample)
Hyperlink Text via HTML
Hyperlink an Image via HTML
Use the ASP.NET server control Hyperlink (Text and Images)
Use a LinkButton and regular Button to navigate to another page via code in the button event handler
Response.Redirect("page.aspx");
Learn how to pass data to another page
introduce the concept of a query string ... observe a Google search query
look at the query string format : ?h1=en & q=web & btnG=Google
use Response.Redirect("file.aspx" + QueryString)
use Request.QueryString["passingvariable" ]
Look at the concept of Session Variables
here values are not passed via Response.Redirect in query string ... variables are private
Session["pw"] = TxtPass.Text (on login page)
TxtDisplay.Text=Session["pw"].ToString() on new page
Can send user back to login screen for invalid entry
Remember to set TextMode to Password for Password TextBox
Offer you the challenge of creating a multi-page site with
a main login page
a second page highlighting your favorite team/musical group
In this Lecture we will
Offer you the challenge of creating a simple slots game having 3 windows
Provide a fully detailed solution after you give it a try ... CasinoSlots
illustrates use of arrays
illustrates use of TryParse
illustrates use of Methods
illustrates use of Image.ImageUrl
In this Lecture we will
Offer you the challenge of creating a Currency Converting web page
Provide a fully detailed solution after you give it a try
illustrates use of IsPostBack
illustrates use of DropDownList
illustrates use of Decimal.TryParse
The most important factor is that double, being implemented as a binary fraction, cannot accurately represent many decimal fractions(like 0.1) at all ... leading to rounding errors and its overall number of digits is smaller since it is 64 - bit wide vs. 128 - bit for decimal.
Use double for non - integer math where the most precise answer isn't necessary.
Use decimal for non - integer math where precision is needed (e.g.money and currency).
illustrates use of accessing CSS attributes through a style collection
controlName.Style["AttributeName"]="AttributeValue"
In this Lecture we will
In this Lecture we will
Learn about the advantages of using Styles
If you want your Web site to have a unique character and be easy for visitors to get around in, it’s important to maintain a consistent look and feel throughout all pages in your site. This involves thinking about what kinds of design elements you might use in your site. Examples of design elements include things like main headings, subheadings, body text, tables, picture borders, lines, and other items that might appear on pages throughout your site.
To maintain a consistent look and feel, it’s best to predefine the exact appearance of all these items in style sheets. Doing so up front saves you a lot of time because you don’t have to style every single heading, table, and picture as you add it to your page. Instead, you just format things normally and they automatically take on the appropriate appearance as you create them.
The real beauty of style sheets goes beyond consistency and ease of use to, well, preserving your sanity. If you ever decide to change the style of some element in your Web site, you don’t have to go through every single page and make the change. You just change the style in the style sheet, and the new style is automatically displayed in every page. The technology you use to create style sheets goes by the name Cascading Style Sheets, or CSS for short.
Learn about the three types of CSS
In a separate file (external)
At the top of a web page document (internal)
Right next to the text it decorates (inline)
External style sheets are separate files full of CSS instructions (with the file extension .css). When any web page includes an external stylesheet, its look and feel will be controlled by this CSS file (unless you decide to override a style using one of these next two types). This is how you change a whole website at once. And that's perfect if you want to keep up with the latest fashion in web pages without rewriting every page!
Internal styles are placed at the top of each web page document, before any of the content is listed. This is the next best thing to external, because they're easy to find, yet allow you to 'override' an external style sheet -- for that special page that wants to be a nonconformist! (see SimpleFormattingRevisited)
Inline styles are placed right where you need them, next to the text or graphic you wish to decorate. You can insert inline styles anywhere in the middle of your HTML code, giving you real freedom to specify each web page element. On the other hand, this can make maintaining web pages a real chore!
Learn how to create different types of Style Sheets using the Style Builder (Under Format menu ... New Style)
Inline Styles (StyleSheetIntro)... use Style Builder and define in current page ... click inside <div>
we take a simple page containing some text, a text box and a button and create a style to improve the look
we add a background color, border, border color
we change the default font and size and style (italic)
we update and edit the style using the CSS properties window
View/CSS properties
View/Manage Styles
External Styles (linking to an external Style Sheet file) (ExternalStyleIntro)
create a folder called stylesheet
right click folder and add new item .. StyleSheet (give it a name)
in design view ...pick New Style (under format menu) then new/existing file
create the style rules in the builder
Choose Selector (HTML tag/Class/ID) then Declaration (property:value)
we style the <body> tag and the <H1> <H2> tags
we create a class
Learn how to link to a style sheet
drag stylesheet to upper left corner of page
to apply a class ... highlight text .. then View/Apply Styles or go to Properties/Class
In this Lecture we will
Learn about the style sheet concept of Flow and Floating Based Layout (FlowBasedLayoutIntro)
create a style (without floats) that defines a
header
left panel
center panel
right panel
NOTE: we use classes here... but the most correct way to define these specific elements we learn in our second example using ids
Take a look at a more complex Floating Layout (ChiarelliElearning)
reviews the concepts of classes, ids , descendent selectors, page wrappers
You use IDs when you have a single element on the page that will take the style. IDs must be unique. They are defined with "#" . Examples of ids are: main-content, header, footer, or left-sidebar.
You use a class when you want to consistently style multiple elements throughout the page/site. A class is defined with a "." Classes are useful when you have, or possibly will have in the future, more than one element that shares the same style. An example may be a div of "comments" or a certain list style to use for related links. Additionally, a given element can have more than one class associated with it, while an element can only have one id. For example, you can give a div two classes whose styles will both take effect.
In this Lecture we will
Learn about ASP.NET’s master pages feature, which allows you to define page templates and reuse them across your website.
Learn that Master pages are similar to ordinary ASP.NET pages. Like ordinary pages, master pages are text files that can
contain HTML, web controls, and code. However, master pages have a different file extension (.master instead of
.aspx), and they can’t be viewed directly by a browser. Instead, master pages must be used by other pages, which
are known as content pages.
Learn that essentially, the master page defines the page structure and the common ingredients. The content pages adopt this structure and just fill it with the appropriate content. For example, if a website such as http://www.amazon.com were created using ASP.NET, a single master page might define the layout for the entire site. Every page would use that master page, and as a result, every page would have the same basic organization and the same title, footer, and so on. However, each page would also insert its specific information, such as product descriptions, book reviews, or search results, into this template.
Create a simple Master Pages and apply to a web page (MasterPageIntro)
create a folder called master
add new item ... Master Page
modify layout ... take a good look at the code in source view
moving ContentPlaceHolder into appropriate locations on page ... this will hold unique content on each page that uses master
you can drag more ContentPlaceHolders as needed from toolbox or copy and paste in source
use the master page
create a new web form ... choose Web Form with Master
editing the master page
In this Lecture we will
Demonstrate how to incorporate a style sheet into a master page (ChiarelliElearning -> ChiarelliElearningMasterStyled)
use a CSS floating layout
put a Content Placeholder in the "MainContent" ID Section
create a web page based on the master page
Deriving a master page from a full CSS Template
using a freely available template (soft_green) ... copy styles.css and images folder over to Visual Studio
create a master page ... remove the content placeholder from <head> section and <div> from body
copy all the <div> content in body of index.html(css template) into body of master page
drag style sheet onto master page
move content placeholder to appropriate position and add other holders where necessary
Demo a master page based on a free CSS Templates
BrownShadow -> BrownShadowMod
You try modifying this free CSS Templates ... Metamorph
In this Lecture we will
Challenge you with a exercise to practice the skills you learned in this section on using Style Sheets and Master Pages
Find a Free CSS Template and implement it into a site of your own interest ... Sports/Music Group etc
The site should use a Master Page which is then used to create subsequent sub pages of the site
Need Resources ? .... check the links from the previous lecture
In this Lecture we will
Learn that many sites today are dynamic and interactive
They display content that is updated frequently ... news, sports, stocks
They allow clients to interact with the site ... shopping sites, auction sites
Learn that interaction requires storage
Learn that creating a database is a way to solve this storage requirement
Demonstrate how Visual Studio can be used to create a database using SQL Express (DataBindingIntro1/DataBindingIntro1VS2022 ...don't execute application ... will be used as the starting point for next lecture where we display the data created here in a GridView and DetailsView)
local file based database engine ... similar to Access with full SQL support for database up to 4 GB
right click on App_Data folder
create a new database (Customers) ... note database explorer window now appears
add a new table then add fields and data types
customerID ... int /FirstName ... varchar(50) /LastName... varchar(50) /CreditLimit... money
varchar means variable length text 1-50 characters
set identity specification for customerID and make it a keyfield
save table (customer.mdf)
edit SQL name from Table to Customer
press Update (choose Update Database)
go over to Server Explorer and Refresh ... the Customer table should now appear under the table section
Now enter data (right click on newly appearing Customer table / show table data)
interact with DB using SQL (right click customer table and select new query ... find all customers whose creditlimit is less than $8000 (Don't save query ... just demo results)
SELECT FirstName, LastName, CreditLimit FROM Customer WHERE CreditLimit < 8000
In this Lecture you will
Learn that the basic principle of data binding is this: you tell a control where to find your data and how you want it
displayed, and the control handles the rest of the details.
ASP.NET data binding works in one direction only. Information moves from a data object into a control. Then the data objects are thrown away, and the page is sent to the client. If the user modifies the data in a data-bound control, your program can update the corresponding record in the database, but nothing happens automatically.
Learn about many of the most powerful databinding controls, such as the GridView and DetailsView, which give you unprecedented control over the presentation of your data, allowing you to format it, change its layout, embed it in other ASP.NET controls, and so on.
Add Databound controls to the Web Form started in the previous lecture (DataBindingIntro2 ... remember to use DataBindingIntro1VS2022 as the starting point for this Lecture )
add a data source to the web form (SqlDataSource)
Create a new connection
choose Data Source (SQL Server DB) and DB File (customer.mdf)
specify columns (*) all ... notice Select statement
press Test Query
add a grid view to display data ... choose data source SqlDataSource1
also demo simple technique ... just drag and drop data table in <DIV> block (IDE adds 2 controls Grid View and SqlDataSource) ... make sure to choose SqlDataSource1
add a details view to a new web form
after initial SqlDataSource is added to a web form there is no need to create that connection again. You only need to add the desired control.
Visual Studio will automatically add the SqlDataSource after you answer the prompts
first drag and drop the DetailsView control
next choose new data source ( SQL DB)
to the left of "New Connection" pick from the drop down menu "ConnectionString"
Now you are accessing the previous data source
choose your columns and test your query
autoformat
enable paging
... other properties (formatting numbers {0:c} currency (edit fields)
grid view and detail view together
configure gridview first then details view (SqlDataSource1)
enable selection in grid view
remove CustomerID/CreditLimit columns (edit columns)
no paging
write code for grid view control's "SelectedIndexChange" event (access through bolt prop)
DetailsView1.PageIndex=GridView1.SelectedIndex
In this Lecture we will
Look at a simple application of databinding related to a music band site in which the data is stored in an Microsoft Access Database (DataDrivenBandBioPage ... using BandDatabase Access 2000 version )
NOTE: These DB Lectures which connect to an Access DB only work with Visual Studio 2019 (32 bit) and will not work with Visual Studio 2022/2026 (64 bit)
add BandDataBase.mdb to AppData folder
Connect to an Access DB using Gridview
add Gridview onto page
choose data source (new data source)... SQL database
choose BandDataBase.mdb in Data Connection screen dropdown menu
configure Select statement ... choose the artist table
Select * From [Artists] Where
Remove "PictureName" field after Gridview appears
Editing Gridview properties
HeaderStyle ... BackColor, BorderColor, BorderStyle, BorderWith
Give you a chance to experiment with the Access Database by creating a GridView control of the Tours table.
... Then make it look nicer... here's a couple of ideas
enable paging
set the PageStyle font size to medium to make the numbers easier to read
for columns choose only ShowDate, City, State, ZipCode and Tournumber
Format the date as {0:MM/dd/yy}
a white border of two pixels would look nice
use Autoformat to change the appearance of the table
In this Lecture we will
Learn how to filter data from the database by creating a Select query (FilteringData)
NOTE: These DB Lectures which connect to an Access DB only work with Visual Studio 2019 (32 bit) and will not work with Visual Studio 2022/2026 (64 bit)
Using the WHERE and ORDER BY clause options where the source is a control like a RadioList or DropDownList
First create the RadioList before adding a GridView (eg. Daniel value 1/Davis 2/Kevin 3/Reginald 4/Thaddeus 5
don't forget to set autopostback to true
What we eventually want is an application where a user selects a radiobutton and the records for the selected band member (1,2,3 ) is displayed in gridview
Next add a GridView to the page
create a new datasource and make a connection to the BandDatabase
select the ArtistJournal table and choose the IDNumber , JournalDate and Remarks fields ... notice how the wizard automatically creates the SQL statement
Next click on the WHERE button to construct the WHERE clause.
Choose the IDNumber in the Column field. For the Operator, choose =. For the Source, select Control. Select RadioButtonList1 from the ControlID list in the Parameter properties section. When we call the query method, we can pass in a value for the IDNumber (2 for example).
Test the query
Test out the page by clicking on the RadioButtonList and choosing an artist to display.
Next let's re-configure our WHERE clause to include AND condition
first add a text box to the form to hold a date
next click on SqlDataSource control and click on Configure Data Source
click on the WHERE button to add a second condition
choose JournalDate, operator >= with control id TxtDate
Click Test Query. The Parameter Values Editor opens and
you are prompted to enter an IDNumber (enter 2) and a Date (enter 1/1/2003).
Lastly we add an ORDER BY clause
first configure data source again
this time click on the ORDER BY button
Select Remarks for the Sort by and choose descending. Notice the changes that occur to the SELECT statement. Click OK.
Code Tweaking
Modifying the SQL statement by using the Query Builder (demo only)
click on the SqlDataSource
select the ellipsis on the SelectQuery property (property window on right side of Visual Studio screen)
In this Lecture we will
Learn how to implement a FormView control which allows us to access CRUD (create,read , update,delete) ... FormViewCrud
GridView displays multiple records in a database in a grid style. We are able to delete, edit and select, but we
are not able to insert. The FormView on the other hand allows for creation of data (new)
We used the DetailsView control to display one record at a time. The FormsView control displays one record at a time as well. So, what is the difference? The basic difference is that the FormsView control allows you to edit more features of the templates
whereas the DetailsView provides a standard block style with limited template editing.
Use an SQL data source
customer.mdf ... Customers2022dfFile.rar
need to make proper connection string (add data source to web form first ... SqlDataSource)
configure data source
choose *
then go into advanced when databinding
generate insert/update/delete statements
use optimistic concurrency
database must use key fields if you want to be able to edit the table through web page
turn on paging in FormView control
Experiment with Web App
adding new records (Create)
editing records (Update)
deleting records (Delete)
viewing the updated table (Read)
... go into DB Explorer and view actual raw table data to observe changes
In this Lecture we will
Challenge you to create a new database with SQL Server Express.
This database will store historical weather data, so you will create a table to keep track of weather details like temperature, rainfall, and so on.
Later you will build an interface to add weather data for all clients, so you will also store the location in this database.
In this Lecture we will
Create an ASP.NET Web Application that implements a DropDown List that is data bound and filters records into a GridView (BandTour)
NOTE: These DB Lectures which connect to an Access DB only work with Visual Studio 2019 (32 bit) and will not work with Visual Studio 2022/2026 (64 bit)
Tour number is in dropdown list (enable PostBack)
Tour Table from BandDatabase is chosen
This data source only contains the field Tour Numbers ... with Return only unique rows specified
Records filtered into GridView
add GridView choosing new datasource (don't use SqlDatasource1)
choose same connection string (ConnectionString BandTour)
when you choose the connection string you want to reuse the same connection string . That's because the connection string only tells the control where the database is located, it doesn't specify any tables or views within the database.
this data source contains all the fields + WHERE clause + ORDER BY
set WHERE clause for TourNumber = DropDownList1
ORDER BY TourNumber and ShowDate
Select Advanced and check the Generate INSERT, UPDATE and DELETE statements as well as the Use optimistic concurrency boxes. Recall that optimistic concurrency will detect whether someone else has modified the record during the time that you pulled it up on your screen and not perform the update.
In the GridView control select Edit Columns. Edit the Selected fields to include TourNumber, ShowDate and City. For the ShowDate column, change the DataFormat property to {0:d}
enable Paging, Deleting and Selection
Set Paging Properties.... Set PageSize to 5
Choose Auto Format and pick a style
In this Lecture we will
Create an ASP.NET Web Application that implements a Calendar control that is used to filter dates for records that are fed in a GridView for display.... A Band Journal Search Page (ArtistJournal)
NOTE: These DB Lectures which connect to an Access DB only work with Visual Studio 2019 (32 bit) and will not work with Visual Studio 2022/2026 (64 bit)
We add a calendar control to the Web Form which will be used to select dates which will be placed into textboxes
Use a single calendar control for the Start Date and the End Date...C# code checks for textbox contents and places the chosen date in the appropriate Start or End date textbox
The textbox entries are then used to filter records into a GridView
note the use of two WHERE clauses
JournalDate >= TxtStart and JournalDate <=TxtEnd
In this Lecture we will
Learn to create a Relational Database and connect it to a ASP.NET Web Form (ASPandRelationalDB)
Look at this Scenario
The "band" wants us to add a Favorite Songs page to the web site. They want it to show a fan’s name, their favorite songs, a comment and if they are a member of the StreetTeam – the group of active fans that help promote the band. Easy enough since this information is all in the Fans table. Here's the hard part ⎯ they also want the Favorite Songs page to show the Album Name, Album Number and Track Number. But that information is in a different table; it's in the AlbumTracks table. So we have some work to do there..... That's where the concept of Relational Databases will come into play
Fields in Fans Table
ID
Password
Name
BirthDate
Gender
City
State
Email
FavoriteSong***
StreetTeam
Comment
Fields in AlbumTracks Table
AlbumNumber
AlbumName
TrackNumber
TrackName ***
If you look carefully at the data in these tables, you see that the information in the FavoriteSong field of the Fans table is the same as the information in the TrackName field of the AlbumTracks table. That is, both of these fields contain song titles. In other words, the data in the Fans table is connected to or "related" to the data in the AlbumTracks table through the fields FavoriteSong and TrackName. This is the essential element of a "relational database" ⎯ information in one table can be related to information in another table through fields that hold common information (in this case song titles). Let's say we wanted the name of the album (AlbumName) that contains a fan's favorite song. We would look for the name of the favorite song in the FavoriteSong field of the Fans table. Then we search for the same song in the TrackName field of the AlbumTracks table. Once we find the matching favorite song in the AlbumTracks table, we get the AlbumName for that particular record.
Here's another example. The Albums table contains a field called AlbumNumber. The AlbumTracks table also contains a field called AlbumNumber. Both fields contain an integer: 1 denoting the first album and 2 denoting the second album (and we hope there will be many more in the future). The information in both tables is related through their AlbumNumber fields. Using the AlbumNumber field in the AlbumTracks table, we can find the same AlbumNumber in the Albums table. Once we know which record has the same AlbumNumber, we can get other information from the Albums table such as Release Year and Label.
First add a GridView and connect it to the BandDatabase
When we configure the Select statement we need to select fields from the Fans and Album Tracks tables
Configure Select using custom SQL (Multi-Table Query-Query Builder) ... selecting fields from two tables
Add the Fans and AlbumTracks Tables to the Query Builder
Check the boxes next to the column names we want to display:
Fans: Name, Favorite Song, Street Team, Comments
Album Tracks: Album Number, Album Name, Track Number, TrackName
FavoriteSong field and TrackName are connecting fields
then click on FavoriteSong and drag toward TrackName (this creates relationship).This will match (or join) the two tables on those column names when their values are equal.
press the "Execute Query Button" ... and then "Test Query"
Start your application. The grid is populated with data from the Fans table and the AlbumTracks table. Note that the same song is listed in the FavoriteSong field and the TrackName field.
clean up the grid ... eliminate redundant information (make TrackName invisible) and move AlbumNumber down once so that it is just before TrackNumber
Sort the records by AlbumNumber then by TrackNumber (need to get back into QueryBuilder)
Click SqlDataSource ... then go over to properties and choose SelectQuery
Click in the Sort Type cell in the AlbumNumber row and select Ascending. The Sort Order will be 1. Click in the Sort Type cell
for the TrackNumber row and select Ascending. The Sort Order will be 2.
In this Lecture we will
Welcome you back ... and introduce the 7 new sections
Discuss the strengths and weaknesses of Web Forms
Highlight where Web Forms fail
Discuss how MVC is an improvement over Web Forms
Highlight some of the features of MVC
Based on Model View Controller pattern
lightweight, fast and secure
designs and codes are neatly separated
it uses pure HTML control or HTML helpers (No server controls)
it uses controller based url
implements no view state
it uses layout at the place of master pages for consistent looks and feel
Discuss "What's the Core part ?"
In this Lecture we will
Take a closer look at the MVC software pattern
The Model
The View
The Controller
Discuss how MVC works in 3 steps
In this Lecture we will
Create our very first ASP.NET MVC Core application (using Visual Studio 2019/2022 Community Edition)
Pick ASP.NET Core Web App
Pick Empty Template
Look at the Startup.cs file and modify the await command to display content on the browser (Pre .NET 6)
Using .NET 6 or higher ... no Startup.cs file anymore
Use Program.cs ... app.MapGet("/", () => "Hello Raptors!");
Run the project
In this Lecture we will
Discuss how the Controller acts as the middleman - it will combine your Model with a View and serve the result to the end - user.
However , neither a Model nor a View is required
The Controller can act on its own for the most basic operations... delivering simple text messages
Learn how to add MVC support to a Web project
In the Startup.cs file in ConfigureServices we add
services.AddMvc();
Learn how modify the Configure() method in the Startup.cs ... so that
your web application knows how to map incoming requests to your controller(s)
UseMvcWithDefaultRoute()
Using Visual Studio 2022 and .NET 6 or higher ... no Startup.cs ... only Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
//app.MapGet("/", () => "Hello Raptors!");
app.UseRouting();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
Learn how to add a Controller
create a new folder called "Controllers"
right click the new folder and select Add...New Item...Controller ... MVC Controller - Empty
name the controller HomeController()
Discuss how the Controller looks like a regular C# class.
It has one method called Index() which will try to return the default view by calling the View() method.
Since we do not have any Views yet we will instead use the command
return Content("Hello Raptors");
In this Lecture we will
Discuss how a View is the visual presentation of the Model returned by the Controller
Create a View for the HomeController instead of just returning a piece of text
create a folder called "Views"and inside that folder create another folder called "Home"
right click on the Home folder and Add ... View
Highlight the contents of the newly create View
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
</head>
<body>
</body>
</html>
looks like standard HTML with the MVC related code at the top called Razor syntax
add some content to the body
<span style="font-size: 18pt;">Hello, <b>Raptors</b> world!</span>
Run the application and discuss how the default routing mechanisms found in MVC automatically routed to the HomeController's Index() method.
Discuss how a call to the View() method causes a number of locations to be searched to find a View with a matching name ... in this case
\project root\Views\name of controller\index.cshtml
this view is then interpreted and returned as output to the browser
In this Lecture we will
Discuss the fact that so far we combined a Controller with a View, but this can only create a simple HTML based web page
Discuss that fact that the idea behind MVC is of course to mix HTML with data generated by the server and this is where the Model comes into play
Learn that the Model is generated by the Controller and then passed to the View which outputs the relevant data to the user
Learn what a model can look like
any kind of object found in the framework
it could be a simple number or string or a complex object instantiated from a class
Learn how to add a Model to our web application
create a folder called "Models"
right click the folder and add a Class ... call it Raptors
public class Raptors
{
public string PlayerName { get; set; }
public DateTime ArrivalDate { get; set; }
}
Learn how the Controller is used to instantiate the Model and then send it to the View
public IActionResult Index()
{
Models.Raptors player = new Models.Raptors()
{
PlayerName = "Kyle Lowry",
ArrivalDate = new DateTime(2012, 3, 24)
};
return View(player);
Learn that a View can work without a Model , but when we want to actually use a Model we need to make the View aware of this and tell it which type we expect the Model to be.
This is done with the Razor @model directive , usually in the top of the View
@model HelloRaptors.Models.Raptors
Now the View knows that it should expect a model of the type Raptors
We can modify the View to include the code that references our model properties
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@Model.PlayerName</title>
</head>
<body>
<b>@Model.PlayerName</b>
<br /> was signed by the Toronto Raptors on @Model.ArrivalDate.ToLongDateString()
</body>
</html>
Discuss how we have now finally come full circle and combined a Model, a View and a Controller (MVC).
In this Lecture we will
Learn that when Microsoft first created the ASP.NET MVC framework it used WebForms to display content. However WebForms weren't as flexible as people were used to . WebForms had too much overhead in the form of the ViewSate, server controls etc.
Learn that Microsoft decided to implement a much simpler and more lightweight language/view engine called Razor. It was released in 2011 as part of MVC version 3.
Learn why we use Razor
The biggest advantage is the fact that you can mix client-side markup (HTML) with server-side code (C#)
In Razor you can reference server-side variables by simply prefixing them with an at-character @
<p>/Welcome to our Toronto Raptors Site ! current date is :
@DateTime.Now.ToString()
</p>
Here we are adding a conditional statement to our view
@if (DateTime.Now.Year >= 2020)
{
<span>The year 2020 has finally arrived!</span>
}
In this Lecture we will
Look at all the basic principles of the Razor syntax
Learn that the @ prefix is not just for accessing simple variables or properties on a class. Its' used for pretty much anything in Razor, including inline control statements, code blocks, comments and much more
Learn how HTML encodes and interprets Razor expressions
@{
var helloWorld = "<b>Welcome to Canada !</b>";
}
<p>@helloWorld</p>
<p>@Html.Raw(helloWorld)</p>
The first line will be HTML encoded as <b>Welcome to Canada!</b>
The second line will be intrepreted thereby making the text bold : Welcome to Canada
Learn to wrap Razor expressions with a set of parentheses. This makes it easier for the parser to understand what your are doing.
@{
var name = "Charlie Chiarelli";
}
Hello, @(name.Substring(0, 4)). Your age is: <b>@(37 + 23).</b>
Learn that if you need to do more than simply accesss variable, Razor allows you to enter a dedicated, multiline code-block by entering a start curly-bracket after the @-operator.
@{
var sum = 32 + 10;
var greeting = "Hello, World!";
var text = "";
text += greeting + " The result is: " + sum;
}
<h2>CodeBlocks</h2>
Text: @text
Learn create a comment in Razor code:
@*
Here's a Razor server-side comment
It won't be rendered to the browser
It can span multiple lines
*@
In this Lecture we will
Learn that just like in regular C# code, you can define variables in Razor to store information for later use.
@*Simple assignment statement*@
@{
string Msg = "Hello, world!";
}
<div>
@Msg
</div>
@*Working with and manipulating variables and applying logic to them
just like you would in C#*@
@{
string helloWorldMsg = "Good day";
if (DateTime.Now.Hour > 17)
{
helloWorldMsg = "Good evening";
}
helloWorldMsg += ", world!";
helloWorldMsg = helloWorldMsg.ToUpper();
}
<div>
@helloWorldMsg
</div>
In this Lecture we will
Learn that when defining the markup for your Views, it's extremely useful to define a conditional statement , which decides whether or not a portion of the View is interpreted and rendered.
Learn that the most common conditional statement is the if-statement and you can use one of these in your Razor code pretty much like you would in regular C#.
@if (DateTime.Now.Year >= 2020)
{
<span>The year 2020 has finally arrived!</span>
}
else
{
<span>We're still waiting for the year of 2020...</span>
}
In this Lecture we will
Learn that looping is an extremely useful programming technique which you can also benefit a lot from in your Razor code. Looping allows you to repeat an action and/or output for a specific amount of iterations.
@{
List<string> names = new List<string>()
{
"Kyle Lowry",
"Pascal Siakim",
"Fred VanVleet",
"OG Anunoby",
"Marc Gasol"
};
}
<ul>
@for (int i = 0; i < names.Count; i++)
{
<li>@names[i]</li>
}
</ul>
<ul>
@foreach (string n in names)
{
<li>@n</li>
}
</ul>
<ul>
@{
int counter = 0;
}
@while (counter < names.Count)
{
<li>@names[counter++]</li>
}
</ul>
In this Lecture we will
Take a deeper look at what a controller is
acts as the middleman
it will combine your Model with a View and serve the result to the end user
neither a Model or View is required , the Controller can act on its own
the Controller is just like any other class so it has a .cs file extension
Learn there are a few things that allow you and the .NET framework to recognize it as an MVC Controller
usually placed in a folder called "Controllers" in the root of your project
the name of the class will usually end with the word Controller eg. "HomeController"
Learn that each browser request is mapped with the particular controller and each controller has several action methods (next lecture) to handle browser requests
In this Lecture we will
Learn that since a Controller is just a regular .NET class it can have fields, properties and methods.
Learn that the methods of a Controller class are interesting because they are the connection between the browser and your application. For that reason the methods of a Controller class are referred to as actions
Learn that all public methods in a Controller class are considered an Action. This means that all your methods in your Controller class can in theory be reached using a URL. So if you have methods in your Controller that you don't want the end-user to be able to call, be sure to mark it as private
Learn about Action Verbs
To gain more control of how your actions are called , you can decorate them with the so-called Action Verbs. These are in fact regular .NET attributes, which will tell the .NET framework how an action can be accessed.
//This Edit Action below can only be accessed with a GET request.
[HttpGet]
public IActionResult Edit()
{
return Content("Edit");
}
In this Lecture we will
Learn that when the Action (method) finishes its work, it will usually return something to the client and that something will usually be implementing the IActionResult interface.
public IActionResult Index()
{
// Here now we are going to return a View with a Model
//First we instantiate a new Raptors object
Models.Raptors player = new Models.Raptors()
{
PlayerName = "Kyle Lowry",
ArrivalDate = new DateTime(2012, 3, 24)
};
return View(player); //pass player object to View method
//Other possible results of controller actions ... besides View()
//Content() returns the specified string to the browser
//PartialView() returns a Partial View (we will cover this type soon)
}
In this Lecture we will
Go more in depth with the topic of Views
Review what a View is
The View is the visual result of a Controller Action
A View contains the markup (HTML) and Razor code and will often be a visual representation of your Model. In other words the Controller generates a Model object and then passes it to the View, which then uses the Model to visually represent the content of the Model to the user
Review where a View is placed
View files are usually placed in a folder called Views in the root of the project.
To make it easier for .NET and your Controllers to locate the proper view you usually create a sub-folder inside your Views folder for each of your controllers bearing the name of the Controller. So, if you have a HomeController and ProductController, your Views folder could have sub-folders with the names "Home" and "Product". Each of these folders would then have one or several views relating to the actions of your controllers.
The Home View .... index.cshtml
The Product View ... Details.cshtml
In this Lecture we will
Learn about View Discovery, a process where ASP.NET MVC will try to guess which View to use , without forcing you to specify it.
Learn that View Discovery works if you follow the conventions we covered in the previous lecture
The filename of the View should match the name of the Action
In other words , the location and naming of your views should follow this convention
/Views/Controller Name/Action Name.cshtml
If it does you can simply call the View() method method from your Controller actions and have the .NET Framework automatically locate the proper View for you
If the framework can't find a matching View using this convention it will look in one more place
/Views/Shared/Action Name.cshtml
In this Lecture we will
Learn that the use of a Model is definitely the cleanest and most common approach to passing data into a View
Learn that a Model doesn't have to be a complex class, it could be something simple as a String, an Integer or a DateTime.
Demonstrate a simple scenario with a Product model
public class Product
{
public string Title { get; set; }
public double Price { get; set; }
}
Next, the connection between the Model and View is made by the ProductController
public IActionResult Details (int id)
{
Product product = new Product();
product.Title = "Basketball";
product.Price = 19.99;
return View(product);
}
Notice how the product instance is passed to the View
Inside the Product/Details View , I can now define the Product class as the Model this View can expect using the @model directive located at the top the View file
@model HelloRaptors.Models.Product
With this is place you can now start using your View Model and its properties
<body>
<h1>@Model.Title</h1>
Price:@Model.Price
</body>
Take a look at Demo Lecture49-RaptorsPassingDataIntoView
In this Lecture we will
Learn that Partial Views allow you to split your Views into several files and discuss why you would want to do that
The most obvious reason is that you can take a part of a View and separate it into a Partial View, allowing you to re-use this specific part in other views as you want.
For instance, if you have a login form on your page, you can put this into a Partial View and insert it wherever you would like for a login form to appear, across multiple pages. As an added benefit, you are making the original View less cluttered, by breaking it up into smaller components.
Learn how to add a Partial View to our current project (Lecture50-RaptorsPartialView)
Partial Views are usually placed in a "Shared" folder inside your View folder, it's filename usually starts with an underscore
Right click on the "Shared" folder ... choose View , name it, and make sure you check "Create as partial view"
Type in some code
<div>Hello Raptor Fans</div>
<div>Today is @DateTime.Now.ToString()</div>
Now we try to reference it from one of our Views
<body>
<h2>The Toronto Raptors!</h2>
@*Here we are referencing our view located in the shared folder
When the View is rendered the content of the
Partial View is automatically injected into the place
where you called the PartialAsync() method
mixing in with the rest of the View
Notice that we didn't have to specify a complete filename
and path for the Partial View -
the Shared folder is automatically searched during the
View Discovery process*@
@await Html.PartialAsync("_RaptorHello")
<br />
<b>@Model.PlayerName</b>
<br /> was signed by the Toronto Raptors on @Model.ArrivalDate.ToLongDateString()
</body>
In this Lecture we will
Review the fact that ever since the first website was created and expanded from a single page to several pages, the need to share specific parts of the pages became obvious. From the same footer at the bottom , to re-using complex layouts.
Review the fact that various server-side technologies have solved this problem in different ways from
the include() statement of PHP .... to
the MasterPages technology of ASP.NET Webforms
... They allow you to re-use all the stuff that would otherwise have to be repeated in each file and even worse: manually edited in each file for even the smallest global change
Learn in ASP.NET MVC you can use something called a Layout, often in combination with Sections.
You can have one or several Layouts in your project and each can include zero or more Sections.
In this Lecture we will
Learn that a Layout file allows you to re-use common markup across multiple web pages in your project. This is done by specifying all the common stuff in a layout file and then referencing this file in all of your sub pages
Learn that in ASP.NET MVC layout files look just like a regular view and they also use the same extension (.cshtml).
<!DOCTYPE html>
<html>
<head>
<title>Layout</title>
</head>
<body>
@RenderBody()
</body>
</html>
Notice how it's almost just a regular HTML file, except for the RenderBody() Razor method
This part is required in a Layout file because it specifies where the content of the page using the Layout should be placed.
A file using the layout could look like this:
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
<p>Hello, world!</p>
ASP.NET will now combine these two files resulting in something like this when the page is returned to the browser
<!DOCTYPE html>
<html>
<head>
<title>Layout</title>
</head>
<body>
<p>Hello, world!</p>
</body>
</html>
Learn how to add a Layout to our current MVC project (Lecture52-RaptorsLayoutFiles)
Layouts are usually placed in a sub-folder of the Views folder called Shared
The filename of Layout views are usually prefixed with an underscore to indicate that this is not a regular View.
With that in mind , right click the Shared folder and create a new View called _Layout
All you need to do now is add your own markup and then add a call to the RenderBody() method in the place where you want the page content to be inserted, like this
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>_Layout</title>
</head>
<body>
<h2>Hello Basketball Fans!</h2>
@*This is where you want the page content to be inserted*@
@RenderBody()
</body>
</html>
Now reference the Layout in the Home/Index.cshtml file
@model HelloRaptors.Models.Raptors
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@Model.PlayerName</title>
</head>
<body>
<h2>Welcome To The Toronto Raptors Site</h2>
<b>@Model.PlayerName</b>
<br /> was signed by the Toronto Raptors on @Model.ArrivalDate.ToLongDateString()
</body>
</html>
In this Lecture we will
Learn that Routing in MVC is the concept of mapping a URL , which is what the end-user will be requesting through their browser, to a method in one of your controllers, which will then send a response back to the browser. The response will often be a View.
Learn about Default Routing
If you are working on a brand new project created from the Empty template in Visual Studio (like we are), then your application doesn't have any routing yet.
Routing is implemented in the Startup.cs file. In the Configure method add UseMvcWithDefaultRoute(). This adds a basic route to your project. If no Controller name is specified the URL will automatically map to the HomeController.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//BASIC ROUTING
//we want the controller to handle the task below in the future
//so our web app needs to know how to map incoming
//requests to your controller ... we need routes
//If no Controller name is specified, the URL will be mapped to the HomeController
app.UseMvcWithDefaultRoute();
//Here are some examples of URL's that now work
//and the Controller/Method combination that they map to:
//http://localhost/ -> HomeController.Index()
//http://localhost/Home/Index -> HomeController.Index()
//http://localhost/Test/ -> HomeController.Test()
//http://localhost/Home/Something -> HomeController.Something()
//Also if you have a controller called UserController
//which has a method called Details()
//you could access it by calling the URL: /User/Details
Using .NET 6 or higher , then Routing is implemented in Program.cs
app.UseRouting();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}" );
In this Lecture we will
Learn to step outside the comfort of the default/catch-all route and define our own routes
You can define a route in several ways and places, but we'll start out doing it in the Startup.cs file, inside the Configure() method
//Creating a custom route
//The first parameter is the name of the route
//The second parameter is the URL template for the rule
//which will direct the rule to the desired Controller and method on that Controller
//When we run the app the browser will automatically display
//the Details page first
app.UseMvc(routes => routes.MapRoute("Default", "{controller=Product}/{action=Details}"));
Using .NET 6 or higher then we use Program.cs
app.MapControllerRoute(
name: "Product",
Pattern: "{controller=Product}/action=Details}/{id?}");
app.MapControllerRoute(
name: "default",
patter: "{controller=Home}/{action=Index}/{id?}");
In this Lecture we will
Review the idea that a Model should represent the current state of the application, as well as the business logic and/or operations
A Model can be any kind of object found in the framework
It could be a simple number or string or an instantiated object from a class
Learn about the concept of Separation of Concerns (Soc)
SoC is achieved by encapsulating information inside a section of code, making each section modular and then having strict control over how each module communicates.
For the MVC pattern , this means that both the View and the Controller can depend on the Model, but the Model doesn't depend on neither the View nor the Controller.
This turns your Models into a module that can exist even outside of the MVC framework eg. for use in a desktop app.
In this Lecture we will
Learn that in the old days of ASP.NET WebForms before MVC was introduced the process of updating a Model was a bit cumbersome.
First, you would have to create a FORM with all the fields that the user could change. When this FORM was posted back to the server, you would have to read each of the FORM fields and assign it to your Model
Learn about the concept of Model Binding which allows your Views to use information from the Model to generate e.g. a FORM for editing the Model, but even more helpful, that allows the receiving method on the Controller to treat the posted values as an object of the Models type, instead of forcing you to read strings from the FORM.
In other words, you can now keep working with objects all the way through the communication between the client and server.
Create a simple example which illustrates how Model Binding is implemented. (Lecture56-RaptorsModelBinding/Lecture56-RaptorsModelBindingAlternateSol)
First we need a Model
public class Player
{
public string PlayerName { get; set; }
public string Position { get; set; }
}
Next in our Controller we now pass an instance of this class to the View.
[HttpGet]
public IActionResult SimpleBinding()
{
return View(new Player() { PlayerName = "Kyle Lowry", Position = "Guard" });
}
Next we let our View know what kind of Model it can expect with the @model directive, we can now use various helper methods to help with the generation of the a FORM
@using (var form = Html.BeginForm())
{
<div>
@Html.LabelFor(m => m.PlayerName)
@Html.TextBoxFor(m => m.PlayerName)
</div>
<div>
@Html.LabelFor(m => m.Position)
@Html.TextBoxFor(m => m.Position)
</div>
<input type="submit" value="Submit" />
}
By default the FORM will be posted back to the same URL that delivered it, so to handle that we need a POST-accepting Controller method to handle the FORM submission
//The [HttpPost] attribute tells the routing engine to send any POST requests
//to that action method
[HttpPost]
public IActionResult SimpleBinding(Player raptor)
{
return Content($"User {raptor.PlayerName} updated !");
}
Notice how our Controller action takes just one parameter , of the same type as the Model we passed into the View originally. The Raptor class. Behind the scenes MVC will now process the FORM and assign the values found in its editable controls to the properties of the Raptor class. This is the magic of Model Binding.
In this Lecture we will
Discuss the fact that sometimes it is relevant for your Views to know more about a property on you Model than just its name and type. For these situations, ASP.NET MVC comes with the concept of DataAnnotations.
DataAnnotations basically allow you to add meta data to a property.
Learn how to implement a simple DataAnnotation (Lecture57-RaptorsDataAnnotations)
In our previous example we had the framework generate a label and an input field for a property. When generating the label, the name of the property was used, but property names are generally not nice to look at for humans.
As an example let's say we want to change the PlayerName property to Player Name.
Here's how you do it with DataAnnotations.
public class Player
{
//Using DataAnnotations
[Display (Name="Player Name")]
public string PlayerName { get; set; }
public string Position { get; set; }
}
Our property keeps its original name, but whenever it's presented to a user, an alternate version is used.
In this Lecture we will
Learn that a lot of the available DataAnnotations are actually directly related to the validation mechanisms found in the ASP.NET MVC framework.
They allow you to enforce various kinds of rules for your properties, which will be used in your Views and in your Controllers, where you will be able to check whether a certain Model is valid in its current state or not (after a FORM submission)
Show you how to add some basic validations (Lecture58-RaptorsModel Validation)
//Adding basic validation using DataAnnotations
//[Required] means that value is required ... can't be NULL
//We also used [StringLength] to make requirements about max and min lengths
[Required]
[StringLength(25)]
[Display (Name="Player Name")]
public string PlayerName { get; set; }
[Required]
[StringLength(50,MinimumLength =3)]
public string Position { get; set; }
Next we need to update the View so that it can display error messages to the user using the helper method ValidationMessageFor(). Note also the use of the ValidationSummary() which displays a summary of the validation errors above all the fields.
@using (var form = Html.BeginForm())
{
@Html.ValidationSummary()
<div>
@Html.LabelFor(m => m.PlayerName)
@Html.TextBoxFor(m => m.PlayerName)
@Html.ValidationMessageFor(m=>m.PlayerName)
</div>
<div>
@Html.LabelFor(m => m.Position)
@Html.TextBoxFor(m => m.Position)
@Html.ValidationMessageFor(m=>m.Position)
</div>
<input type="submit" value="Submit" />
}
Lastly we need the Controller to serve the View, as well as handle the POST request when the FORM is submitted. The interesting part here is where we check the IsValid property. Depending on the data you submitted it will be either true or false, based on the validation rules we defined for the Model. With this in place you can now prevent a Model from being saved eg. to a database unless it's completely valid. We simply return the View and the current Model state , if there are any validation errors.
[HttpPost]
public IActionResult SimpleBinding(Player raptor)
{
if (ModelState.IsValid)
{
return Content("Thank you");
}
else
{
//once the form is submitted and if there are validation errors
//we return the FORM to the user, so that they can see and fix
//these errors. We do that by simply returning the View and the
//current Model state , if there are nay validation errors.
return View(raptor);
//return Content("Model could not be validated");
}
In this Lecture we will
We recap some the validation-related DataAnnotations
[Required (AllowEmptyStrings=true)]
[StringLength(50,MinimumLength=3)]
[Range(1,100)]
[Compare("PasswordRepeated")]
[CreditCard] validates that the property has a credit card format
[EmailAddress] validates that the property has an email format
[Phone] validates that the property has a telephone number format
[Url] validates that the property has a URL format
In this Lecture we will
Learn how Microsoft came up with the concept of Tag helpers
Learn that ASP.NET WebForms introduced the concept of controls to the world of coding for the web
This was a well known concept when creating applications for the desktop but not for the web
Controls allowed you to get a lot of out of the box functionality by writing tags which were then intrepreted by ASP.NET and turned into regular HTML and CSS before reaching the browser
Microsoft implemented server controls for all the common HTML controls like asp:Textbox (which would be rendered as an INPUT HTML tag)
Unfortunately, the major complaint about WebForm server controls was the lack of control over the resulting HTML. ASP.NET WebForms were in charge of generating the output to the browser and this became a problem for two reasons
Sometimes the resulting HTML would not work in all browsers
Second of all a lot of purists demanded the HTML be formatted in a specific way, lowercase or uppercase
Learn about the transition from server controls to HTML helpers
Microsoft decided to completely leave out server controls when they implemented MVC.
For this reason they invented the Razor language which allowed you to combine programming logic like IF statements and loops with regular markup.
But missing was the ability to connect the Model data with its visual representation in the View. For this reason they introduced HTML helpers.
the concept of HTML helpers allowed you to generate markup for your Model data, especially when generating FORM's and its elements.
For instance if you were to create an INPUT field for the PlayerName property of our Model, you would write the markup like this:
<input type="text" name="PlayerName" value="@model.PlayerName" />
With the introduction of HTML Helpers you could instead call the TextBoxFor() method on the Html helper class to have this tag generated automatically
@Html.TextBoxFor(model => model.PlayerName)
It's shorter and you get the added benefit of compile time checking of the Razor code.
Learn about the transition from HTML helpers to Tag helpers
Thanks to HTML helpers, binding your Model data together with the markup in your Views is now a lot easier, but a problem from the days of server controls still exists:
Since the markup has been abstracted into helper methods, these helper methods are now responsible for generating your HTML and they also have to be flexible enough to support all the ways you might want to use them. Consider our example from before, but now imagine that we want to add a couple of properties with values to the HTML generated:
@Html.TextBoxFor(model => model.PlayerName, new { @class = "form-control", style = "font-weight: bold; font-size: 120%;" })
The example is now way less elegant than it was before, just because we wanted to add a class and style property with values to the INPUT tag.
Microsoft decided to solve this problem by taking the best from both worlds: With Tag helpers, you write regular HTML tags instead of Razor commands, but you can mix in custom server-side attributes which will allow you to, for instance, easily bind to data from your model. With Tag helpers, we can rewrite our previous example to something like this:
<input asp-for="PlayerName" class="form-control" style="font-weight: bold; font-size: 120%;" />
Notice that the ONLY thing that doesn't look like a regular HTML tag/attribute setup is the asp-for attribute. This is how MVC knows which property from the Model that this control is created for - this information will be used to generate the Name of the INPUT field ("PlayerName" in this case) and the value will be automatically added as well.
Discuss the use of Tag Helpers vs HTML Helpers
HTML helpers are still relevant and a viable choice in ASP.NET MVC. You may prefer their syntax or the way they work, but on top on that, there are still things they can do, which Tag helpers cannot.
For this reason you will see me use both Tag helpers and HTML helpers.
In this Lecture we will
Discuss how one of the most common ways of taking input from the user in a web application is through the query string
In classic ASP you would deal directly with the query string but MVC has abstracted most query string handling into parameters in Controller actions
Review what a query string is
The query string is the part of the URL that comes after a question mark character. So in a URL like this:
https://www.google.com/search?q=test&oq=hello
Everything after the ? character is considered the query string. In this case there are two parameters: One called q and one called oq with values "test" and "hello"
The query string is usually appended as the result of the user clicking on a link or submitting a FORM. This allows the same file on your server to handle multiple situations - it can vary the output returned based on the input through the query string
Learn how to access the query string
To access the query string we use the HttpContext class property of Query
public IActionResult QueryTest()
{
string name = "Pascal Siakim";
if (!String.IsNullOrEmpty(HttpContext.Request.Query["name"]))
name = HttpContext.Request.Query["name"];
return Content("Name from query string " + name);
//To test this out use a URL like this
// /Home/QueryTest?name=Serge Ibaka
}
In this Lecture we will
Learn that HTTP the protocol that takes care of the communication between a server and a client on the web is known as a stateless protocol. In other words, if a user requests two pages on a server, no information will be shared between these two request automatically
Instead a developer will have to rely on something called cookies to share information between the requests. This is extremely useful in a lot of situations eg. to keep a user logged in between several requests.
Learn what a cookie is
A cookie is basically a physical plain-text file stored by the client (usually a browser) tied to a specific website. The client will then allow this specific website to read the information stored in this file on subsequent requests, basically allowing the server to store information for later use.
Learn how to set and read cookies
HttpContext.Response.Cookies.Append("user_id", "1");
this declares a cookie named user_id with a value of 1
var userId = HttpContext.Request.Cookies["user_id"];
this gets back the value of the cookie user_id
Create a simple code block which determines if a user has visited our site before or not
public IActionResult Index()
{
//Working with Cookies
//Simple example which determines whether has visited this site before
CookieOptions cookieOptions = new CookieOptions();
cookieOptions.Expires = new DateTimeOffset(DateTime.Now.AddDays(7));
if (!HttpContext.Request.Cookies.ContainsKey("first_request"))
{
HttpContext.Response.Cookies.Append("first_request", DateTime.Now.ToString(), cookieOptions);
return Content("Welcome, new visitor");
}
else
{
DateTime firstRequest = DateTime.Parse(HttpContext.Request.Cookies["first_request"]);
return Content("Welcome back, user! You first visited us on " + firstRequest.ToString());
}
Here we check whether the visitor has visited the page before by always checking for the presence of a cookie with the name of "first_request" - if this cookie is not present, we add it, while setting the value to the current date and time. If the cookie is present we can assume that the visitor has visited the page before and we can even tell the visitor when the first visit occurred.
Notice also the use of an optional third parameter to the Append() method. You can pass an instance of the CookieOptions class. It allows you to adjust several important aspects of your cookie eg. how long it should stay alive.
In this Lecture we will
Briefly discuss the importance of using a database in a web application ( a full DB connected application will created in the next section)
For most programming languages and frameworks the ability to communicate with a database is very important. This is even more true for frameworks targeted towards the web where you will quickly run into the need for a database to store date for you website
Learn that when using Visual Studio the best database to use is Microsoft's SQL Server Express which is built into the IDE.
You can work with the database (design tables, edit rows etc) directly from Visual Studio
It scales very well - you can use it for a small, personal website and if that website grows with thousand of visitors, SQL Server will still have more than enough power and scalability to support it.
In this Lecture we will
Learn the basics of building an ASP.NET Core MVC web app. The app manages a database of Player Info for the Toronto Raptors Professional Basketball Team.
Over the next 10 Lectures you learn how to:
Create a web app.
Add and scaffold a model.
Work with a database.
Add search and validation.
At the end, you will have an app that can manage and display player data.
Start the process of creating the app by :
Creating a new project ... ASP.NET Core Web Application called MVCBasketball
Select Web Application (Model-View-Controller)
Test run the app
In this Lecture we will
Review that the MVC architectural pattern separates an app intro three main components: Model, View and Controller. The MVC pattern helps you create apps that are more testable and easier to update than traditional apps.
Models: Classes that represent the data of the app. The model classes use validation logic to enforce business rules for that data. Typically, model objects retrieve and store model state in a database. In this tutorial, a Raptor model retrieves player data from a database, provides it to the view or updates it. Updated data is written to a database.
Views: Views are the components that display the app's user interface (UI). Generally, this UI displays the model data.
Controllers: Classes that handle browser requests. They retrieve model data and call view templates that return a response. In an MVC app, the view only displays information; the controller handles and responds to user input and interaction. For example, the controller handles route data and query-string values, and passes these values to the model. The model might use these values to query the database. For example, https://localhost:5001/Home/About has route data of Home (the controller) and About (the action method to call on the home controller).
Create the HomeController
Right-Click Controllers ... Add -> Controller
Select the MVC Controller Empty template
Go through a series of modifications to the Controller contents
replace return View() with return Content("This is the default action")
create a new method
public IActionResult Welcome()
{
return "This is the Welcome action method...";
}
Every public method in a controller is callable as an HTTP endpoint. In the samples above both methods return a string. The first is called using /Home and the second is called using /Home/ Welcome
Learn that MVC invokes controller classes (and the action methods within them) depending on the incoming URL. The default URL routing logic is set in the Configure method in Startup.cs
//The default URL routing logic
//When you browse to the app and don't supply any URL segments
//it defaults to the "Home" controller and the "Index" method
//Note: the trailing ? (in id?) indicates the id parameter is optional
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Learn how to modify the Welcome method (action) to handle passing some parameter information from the URL to the Controller
/Home/Welcome?playername=Kyl&age=30
public string Welcome(string playername,int age=24)
{
//I'm using HtmlEncoder to protect the app from malicious input
return HtmlEncoder.Default.Encode($"Hello {playername}, age is: {age}");
//The $ special character identifies a string literal as an
//interpolated string. An interpolated string is a string literal
//that might contain interpolation expressions.
//When an interpolated string is resolved to a result string,
//items with interpolation expressions are replaced by the
//string representations of the expression results.
}
Discuss the fact that in these examples the controller has been doing the VC portion of MVC - that is the view and controller work. The controller is returning HTML directly. Generally you don't want controllers returning HTML directly since that becomes very cumbersome to code and maintain. Instead you typically use a separate Razor view template file to help generate the HTML response. We do that in the next lecture.
In this Lecture we will
Modify the HomeController class to use Razor view files to cleanly encapsulate the process of generating HTML responses to a client
Razor based view templates have a .cshtml file extension. They provide an elegant way to create HTML output with C#
Currently the Index method returns a string with a message that is hard-coded . We will replace this with
public IActionResult Index()
{
return View();
//return Content("This is the default action ");
}
This code calls the controller's View method. The Index method returns an IActionResult not a type like string
Learn to to create the corresponding View for the action method above.
Right click on the Views folder and add a sub-folder called Home
Right click on this Home folder and Add ... New Item...View
Use Name ... index.cshtml and then add
@{
ViewData["Title"] = "Toronto Raptors Roster";
}
<h2>Team Roster</h2>
<p>Hello from our View Template</p>
Learn how to change Views and Layout pages
Note that when you click on all the links they each show the same menu layout. The menu layout is implemented in the Views/Shared/_Layout.cshtml file.
Open the _Layout.cshtml file. You will notice that the template allows you to specify the HTML container layout of your site in one place and then apply it across multiple pages in your site.
Notice the @RenderBody() line. This is a placeholder where all the view specific pages you create show up, wrapped in the layout page.
Change the title, footer and menu link in the layout file
Learn about the significance of the _ViewStart.cshtml file
@{
Layout = "_Layout";
}
This file brings in the Views/_Layout.cshtml to each view. The Layout property can be used to set a different layout view, or set it to null so no layout file will be used.
Notice how the content in the Index.cshtml view template was merged with the Views/Shared/_Layout.cshtml view template and a single HTML response was sent to the browser.
Learn how to pass data from the Controller to the View using a ViewData dictionary
currently the Welcome method in the HomeController class takes a playername and age parameter and then outputs the values directly to the browser. Rather than have the controller render this response as a string, we are going to use a view template instead.
public IActionResult Welcome(string playername,int age=2)
{
//The ViewData dictionary is a dynamic object,
//which means any type can be used;
//the ViewData object has no defined properties
//until you put something inside it.
//The MVC model binding system automatically maps
//the named parameters(playername and age) from the query string
//in the address bar to parameters in your method.
ViewData["Message"] = "Hello " + playername;
ViewData["Age"] = age;
//The ViewData dictionary object contains data that will be passed
//to the view.
return View();
}
Now we need to create a new Razor page to match the Welcome action (inside the Views/Home folder) called Welcome.cshtml
@{
ViewData["Title"] = "Welcome";
}
<h2>Welcome</h2>
<ul>
@for (int i = 0; i < (int)ViewData["Age"] ;i++)
{
<li>@ViewData["Message"]</li>
}
</ul>
@*browse to the URL
/Home/Welcome?playername=Kyle Lowry&age=24
Data is taken from the URL and passed to the controller using the
MVC model binder.
The controller packages the data into a ViewData dictionary and
passes that object to the view.
The view then renders the data as HTML to the browser*@
Conclude with the fact that in this Lecture the ViewData dictionary was used to pass data from the controller to a view. In our next Lecture a view Model is used to pass data from a controller to a view. The view model approach to passing data is generally much preferred over the ViewData dictionary approach. In the next Lecture a database of player stats is created.
In this Lecture we will
Add classes for managing player info in a database. These classes will be the "Model" part of the MVC app.
Learn how to implement these classes with the Entity Framework Core to work with a database.
EF Core is an object-relational mapping (ORM) framework that simplifies the data access code that you have to write.
Start off by adding a data model class
Right click the Models folder... Add...Class...Raptors
Add the following properties to the Raptors class:
public class Raptors
{
public int Id { get; set; }
public int PlayerNum { get; set; }
public string PlayerName { get; set; }
public string PlayerPosition { get; set; }
public string PlayerHeight { get; set; }
public double PlayerSalary { get; set; }
}
Learn how to Scaffold the Raptors model
The scaffolding tool produces pages for Create, Read, Update and Delete (CRUD) for the Raptors model
In the Solution Explorer, right click the Controllers folder... Add... New Scaffolded Item
In the Add Scaffold dialog, select MVC Controller with views, using Entity Framework ... Add
Complete the Add Controller dialog
Model class: Raptors
Data Context class ... Select the + icion and add the default
Views: Keep the default of each option checked
Controller name: Keep the default... Select Add
Visual Studio creates:
In the data folder an Entity Framework Core (database context class) MVCBasketballContext.cs
a Raptors Controller
Razor view files for Create, Delete, Details, Edit and Index pages (Views/Raptors/*.cshtml)
This automatic creation of the database context and CRUD action methods and views is known as scaffolding.
Learn that if you run the app now you will get an error. You need to create the database, and you use the EF Core Migrations feature to do that. Migrations lets you create a database that matches your data model and update the database schema when you model changes.
Initial Migration
From the Tools menu , select NuGet Package Manager... Package Manager Console
In the console enter the following commands
Add-Migration Initial
Update-Database
Test the app
Test the Create link...Enter and submit data
Test the Edit, Details and Delete links
Recall how in an earlier Lecture we had the controller pass data or objects to a view using the ViewData dictionary. But MVC also provides the ability to pass strongly typed model objects to a view. The scaffolding mechanisim used this approach with the RaptorsController class and views when it created the methods and views.
Examine the generated Details method in the RaptorsController
public async Task<IActionResult> Details(int? id)
{
//the id parameter is defined as a nullable type (int ?)
//in case an ID value isn't provided
if (id == null)
{
return NotFound();
}
//a lambda expression is passed in to the FirstOrDefaultAsync
//to select player entities that match the route data or query string
//if a player is found, an instance of the Raptors model is passed to
//the Details view
var raptors = await _context.Raptors
.FirstOrDefaultAsync(m => m.Id == id);
if (raptors == null)
{
return NotFound();
}
return View(raptors);
}
Examine the contents of the Views/Raptors/Details.cshtml
@model MVCBasketball.Models.Raptors
@{
ViewData["Title"] = "Details";
}
<h1>Details</h1>
<div>
<h4>Raptors</h4>
<hr />
<dl class="row">
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.PlayerNum)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.PlayerNum)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.PlayerName)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.PlayerName)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.PlayerPosition)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.PlayerPosition)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.PlayerHeight)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.PlayerHeight)
</dd>
<dt class = "col-sm-2">
@Html.DisplayNameFor(model => model.PlayerSalary)
</dt>
<dd class = "col-sm-10">
@Html.DisplayFor(model => model.PlayerSalary)
</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>
By including a @model statement at the top of the view file, you can specify the type of object that the view expects.
In this Lecture we will
Take a look at using SQL Server Express LocalDB
LocalDB is a lightweight version of the SQL Server Express Database Engine that's targeted for program development in Visual Studio
LocalDB starts on demand and runs in user mode so there's no complex configurations.
By default, LocalDB database creates .mdf files
From the View menu open SQL Server Object Explorer
Click on Databases ... then MVCBasketballContext ... then Tables
Now right click Raptors and pick View Designer
Note the key icon next to ID. By default EF will make a property named ID the primary key.
Right click again on Raptors and pick View Data ... The database is empty
Learn how to seed the database
Create a new class named SeedData in the Models folder. Replace the generated code with the following.
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MVCBasketballContext(
serviceProvider.GetRequiredService<
DbContextOptions<MVCBasketballContext>>()))
{
// Look for any player info
if (context.Raptors.Any())
{
return; // DB has been seeded
}
context.Raptors.AddRange(
new Raptors
{
PlayerNum = 9,
PlayerName = "Serge Ibaka",
PlayerPosition = "Forward",
PlayerHeight = "7-0",
PlayerSalary=22271604
},
new Raptors
{
PlayerNum = 33,
PlayerName = "Marc Gasol",
PlayerPosition = "Centre",
PlayerHeight = "6-11",
PlayerSalary = 25595700
},
new Raptors
{
PlayerNum = 7,
PlayerName = "Kyle Lowry",
PlayerPosition = "Guard",
PlayerHeight = "6-0",
PlayerSalary = 34996296
},
new Raptors
{
PlayerNum = 43,
PlayerName = "Pascal Siakam",
PlayerPosition = "Forward",
PlayerHeight = "6-9",
PlayerSalary = 2351839
},
new Raptors
{
PlayerNum = 23,
PlayerName = "Fred VanVleet",
PlayerPosition = "Guard",
PlayerHeight = "6-1",
PlayerSalary = 9346153
},
new Raptors
{
PlayerNum = 24,
PlayerName = "Norman Powell",
PlayerPosition = "Guard",
PlayerHeight = "6-3",
PlayerSalary = 10116576
},
new Raptors
{
PlayerNum = 3,
PlayerName = "OG Anunoby",
PlayerPosition = "Forward",
PlayerHeight = "6-7",
PlayerSalary = 2281800
}
);
context.SaveChanges();
}
}
Note: If there are any records in the DB, the seed initializer returns and no records are added.
Next we need to add the seed initializer
replace the contents of Program.cs with the following code.
public static void Main(string[] args)
{
//Here we start the process of adding the seed initializer
var host = CreateWebHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<MVCBasketballContext>();
context.Database.Migrate();
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
Test the app ... if there is data in the database delete all the records first either using the delete links in the browser app or right in SQL Server Express LocalDB
MVCBasketball (pre .NET 6 version)
MVCBballNet7WorkingWithSQLwithSeed (.NET 6+ version)
MVCBballNet7WorkingWithSQLwithoutSeed (.NET 6+ version)
In this Lecture we will
Start by modifying Raptors.cs in the Models folder to update the field (column) titles .
public class Raptors
{
public int Id { get; set; }
[Display(Name= "Player Number")]
public int PlayerNum { get; set; }
[Display(Name = "Player Name")]
public string PlayerName { get; set; }
[Display(Name = "Position ")]
public string PlayerPosition { get; set; }
[Display(Name = "Height")]
public string PlayerHeight { get; set; }
[Display(Name = "Salary")]
[Column(TypeName="double(18,2)")]
public double PlayerSalary { get; set; }
}
The Display attribute specifies what to display for the name of a field (in this case "Player Number" instead of "PlayerNumber"
Take a look at the two "Edit" Controller actions methods
The code below shows the HTTP GET Edit method which fetches the movie and populates the edit form generated by the Edit.cshtml Razor file
// GET: Raptors/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var raptors = await _context.Raptors.FindAsync(id);
if (raptors == null)
{
return NotFound();
}
return View(raptors);
}
The code below shows the HTTP POST edit method which processes the posted player info values;
// POST: Raptors/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,PlayerNum,PlayerName,PlayerPosition,PlayerHeight,PlayerSalary")] Raptors raptors)
{
if (id != raptors.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(raptors);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!RaptorsExists(raptors.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(raptors);
}
Notice how the second Edit action method is preceded by the [HttpPost] attribute. The HttpPost attribute specifies that this Edit method can be invoked only for POST requests. You could apply the [HttpGet] attribute to the first edit method, but that's not necessary because [HttpGet] is the default.
The ValidateAntiForgeryToken attribute is used to prevent forgery of a request and is paired up with an anti-forgery token generated in the edit view file (Views/Raptors/Edit.cshtml).
The [Bind] attribute is one way to protect against over-posting. You should only include properties i the [Bind] attribute that you want to change.
The HttpGet Edit method takes the Player ID parameter, looks up the Player using the Entity Framework FindAsync method, and returns the selected Player to the Edit view. If a movie cannot be found, NotFound (HTTP 404) is returned.
Learn that when the scaffolding system create the Edit view, it examined the Raptors class and created code to render <label> and <input> elements for each property of the class. Below we see the Edit view that was generated by the Visual Studio scaffolding system
@model MVCBasketball.Models.Raptors
@{
ViewData["Title"] = "Edit";
}
<h1>Edit</h1>
<h4>Raptors</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" />
<div class="form-group">
<label asp-for="PlayerNum" class="control-label"></label>
<input asp-for="PlayerNum" class="form-control" />
<span asp-validation-for="PlayerNum" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="PlayerName" class="control-label"></label>
<input asp-for="PlayerName" class="form-control" />
<span asp-validation-for="PlayerName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="PlayerPosition" class="control-label"></label>
<input asp-for="PlayerPosition" class="form-control" />
<span asp-validation-for="PlayerPosition" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="PlayerHeight" class="control-label"></label>
<input asp-for="PlayerHeight" class="form-control" />
<span asp-validation-for="PlayerHeight" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="PlayerSalary" class="control-label"></label>
<input asp-for="PlayerSalary" class="form-control" />
<span asp-validation-for="PlayerSalary" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
Note how the scaffolded code uses several Tag Helper methods to streamline the HTML markup.
The Label Tag Helper displays the name of the field and the Input Tag Helper renders an HTML <input. element.
Run the application and navigate to the /Raptors URL. Click an Edit link. In the browser view the source for the page. The <input>elements are in an HTML <form> element whose action attribute is set to post to the /Raptors/Edit/id URL. The form data will be posted to the server when the Save button is clicked. The last line before the closing </form> element shows the hidden XSRF token generated by the Form Tag Helper.
In this Lecture we will
Learn how to implement a search by "Position" in our web application
Learn we need to update the Index action method found inside the Controllers/RaptorsController.cs with the following code:
public async Task<IActionResult> Index(string searchString)
{
//This creates a LINQ query to select the player info
//The query is only defined at this point it has not been
//run against the database
var raptor = from m in _context.Raptors
select m;
//If the searchString parameter contains a string, the basketball
//query is modified to filter on the value of the string string
if (!String.IsNullOrEmpty(searchString))
{
raptor = raptor.Where(s => s.PlayerPosition.Contains(searchString));
}
return View(await raptor.ToListAsync());
//To test this out try the following
//Navigate to /Raptors/Index?searchString=Guard
}
The s=> s.PlayerPosition.Contains() code above is a Lambda Expression. Lambdas are used in method-based LINQ queries as arguments to standard query operator methods asuch as the Where method or Contains (used in the code above).
Note: The Contains method is run on the database not in the C# code shown above.
Learn that you can't expect users to modify the URL every time they want to search by "position". So now we will add UI elements to help filter player positions.
Open the Views/Raptors/Index.cshtml and add the <form> markup below
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-contorller="Raptors" asp-action="Index">
<p>
Position:<input type="text" name="searchString" />
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
Note the HTML <form> tag uses the Form Tag Helper so when you submit the form, the filter string is posted to the Index action of the Raptors controller.
Test out the filter
In this Lecture we will
Learn how to add a new field called PlayerCollege to the application
First we add the field PlayerCollege to Models/Raptors.cs
public class Raptors
{
public int Id { get; set; }
[Display(Name= "Player Number")]
public int PlayerNum { get; set; }
[Display(Name = "Player Name")]
public string PlayerName { get; set; }
[Display(Name = "Position ")]
public string PlayerPosition { get; set; }
[Display(Name = "Height")]
public string PlayerHeight { get; set; }
[Display(Name = "Salary")]
[Column(TypeName="double(18,2)")]
public double PlayerSalary { get; set; }
[Display(Name ="College")]
public string PlayerCollege { get; set; }
}
Next because we've added a new field to the Raptors class you need to update the binding white list so this new property will be included . In the RaptorsController.cs we update the [Bind] attribute for both the Create and Edit action methods to include the PlayerCollege property:
public async Task<IActionResult> Create([Bind("Id,PlayerNum,PlayerName,PlayerPosition,PlayerHeight,PlayerSalary,PlayerCollege")] Raptors raptors)
{
if (ModelState.IsValid)
{
_context.Add(raptors);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(raptors);
}
public async Task<IActionResult> Edit(int id, [Bind("Id,PlayerNum,PlayerName,PlayerPosition,PlayerHeight,PlayerSalary,PlayerCollege")] Raptors raptors)
{
if (id != raptors.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(raptors);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!RaptorsExists(raptors.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(raptors);
}
Next we need to update the view templates in order to display, create and edit the new PlayerCollege property in the browser view
Edit the /Views/Raptors/Index.cshtml and add a PlayerCollege field
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.PlayerNum)
</th>
<th>
@Html.DisplayNameFor(model => model.PlayerName)
</th>
<th>
@Html.DisplayNameFor(model => model.PlayerPosition)
</th>
<th>
@Html.DisplayNameFor(model => model.PlayerHeight)
</th>
<th>
@Html.DisplayNameFor(model => model.PlayerSalary)
</th>
<th>
@Html.DisplayNameFor(model => model.PlayerCollege)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.PlayerNum)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlayerName)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlayerPosition)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlayerHeight)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlayerSalary)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlayerCollege)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Update the /Views/Raptors/Create.cshtml with a PlayerCollege field. You can copy/paste the previous "form group" and let IntelliSense help you update the fields.
Update the remaining templates.
Next we access the SQL Server Object Explorer windows ... go into the appropriate database and hand enter the new field called "PlayerCollege". Lastly hand update in SQL Server all the players.
... If you are loading in the demo solution for this lecture you may need to go to the Package Manager Console and enter
add-migration college
update-database
In this Lecture we will
Learn what it means to keep things DRY
One of the design tenets of MVC is DRY ("Don't Repeat Yourself"). ASP.NET Core MVC encourages you to specify functionality or behavior only once, and then have it be reflected everywhere in an app. This reduces the amount of code you need to write and makes the code you do write less error prone, easier to test, and easier to maintain.
The validation support provided by MVC and Entity Framework Core Code First is a good example of the DRY principle in action. You can declaratively specify validation rules in one place (in the model class) and the rules are enforced everywhere in the app.
Learn how to add validation rules to the Raptors model
The DataAnnotations namespace provides a set of built-in validation attributes that are applied declaratively to a class or property. DataAnnotations also contains formatting attributes like DataType that help with formatting and don't provide any validation.
Update the Raptor class to take advantage of the built in Required, StringLength and Range validation attributes
public class Raptors
{
public int Id { get; set; }
[Range(1,100)] //between 1 and 100
[Required] //must have a value
[Display(Name= "Player Number")]
public int PlayerNum { get; set; }
[StringLength(60, MinimumLength=3)] //maximum and minimum length of string
[Required]
[Display(Name = "Player Name")]
public string PlayerName { get; set; }
[Display(Name = "Position ")]
[StringLength(60, MinimumLength =3)]
[Required]
public string PlayerPosition { get; set; }
[Display(Name = "Height")]
[Required]
public string PlayerHeight { get; set; }
[Display(Name = "Salary")]
[Range(1,100000000)]
[DataType(DataType.Currency)] //lots of other DataType attributes available
[Required] //Date, Time , PhoneNumber, EmailAddress
public double PlayerSalary { get; set; }
[Display(Name ="College")]
[StringLength(60, MinimumLength = 3)]
public string PlayerCollege { get; set; }
}
The validation attributes specify behavior that you want to enforce on the model properties they're applied to:
The Required and MinimumLength attributes indicate that a property must have a value; but nothing prevents a user from entering white space to satisfy this validation.
Having validation rules automatically enforced by ASP.NET Core helps make your app more robust. It also ensures that you can't forget to validate something and inadvertently let bad data into the database.
Run the application and navigate to the Raptors controller. Tap the Create New link and add a new player. Fill out the form with some invalid values. As soon as jQuery client side validation detects the error, it displays an error message.
Notice how the form has automatically rendered an appropriate validation error message in each field containing an invalid value. The errors are enforced both client-side (using JavaScript and jQuery) and server-side (in case a user has JavaScript disabled).
A significant benefit is that you didn't need to change a single line of code in the RaptorsController class or in the Create.cshtml view in order to enable this validation UI. The controller and views you created earlier in these Lectures automatically picked up the validation rules that you specified by using validation attributes on the properties of the Raptors model class. Test validation using the Edit action method, and the same validation is applied.
The form data isn't sent to the server until there are no client side validation errors.
In this Lecture we will
Open the Raptors Controller and examine the Details method
// GET: Raptors/Details/5
public async Task<IActionResult> Details(int? id)
{
//The id parameter is generally passed as route data
//for example /Raptors/Details/1 ... sets:
//the controller to the Raptors controller
//the action to details
//the id to 1
//the id parameter is defined as a nullable type (int ?)
//in case an ID value isn't provided
if (id == null)
{
return NotFound();
}
//a lambda epression is passed in to the FirstOrDefaultAsync
//to select player entities that match the route data or query string
//if a player is found, an instance of the Raptors model is passed to
//the Details view
var raptors = await _context.Raptors
.FirstOrDefaultAsync(m => m.Id == id);
if (raptors == null)
{
return NotFound();
}
return View(raptors);
}
The MVC scaffolding engine that created this action method adds a comment showing an HTTP request that invokes the method. In this case it's a GET request with three URL segments, the Raptors controller, the Details method, and an id value. Recall these segments are defined in Startup.cs.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
EF makes it easy to search for data using the FirstOrDefaultAsync method. An important security feature built into the method is that the code verifies that the search method has found a movie before it tries to do anything with it. For example, a hacker could introduce errors into the site by changing the URL created by the links from http://localhost:{PORT}/Raptors/Details/1 to something like http://localhost:{PORT}/Raptors/Details/12345 (or some other value that doesn't represent an actual Raptor player ). If you didn't check for a null raptor, the app would throw an exception.
Examine the Delete and DeleteConfirmed methods
// GET: Raptors/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
var raptors = await _context.Raptors
.FirstOrDefaultAsync(m => m.Id == id);
if (raptors == null)
{
return NotFound();
}
return View(raptors);
}
// POST: Raptors/Delete/5 ActionName below performs mapping
[HttpPost, ActionName("Delete")] //for the routing system so that a URL that includes
[ValidateAntiForgeryToken] // /Delete/ for a POST request will find DeleteConfirmed
public async Task<IActionResult> DeleteConfirmed(int id)
{
var raptors = await _context.Raptors.FindAsync(id);
_context.Raptors.Remove(raptors);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
Note that the HTTP GET Delete method doesn't delete the specified Raptor player, it returns a view of the player where you can submit (HttpPost) the deletion. Performing a delete operation in response to a GET request (or for that matter, performing an edit operation, create operation, or any other operation that changes data) opens up a security hole.
The [HttpPost] method that deletes the data is named DeleteConfirmed to give the HTTP POST method a unique signature or name.
Offer you several Challenges to review and extend your MVC skills
MVCProject.pdf
RevisitWebFormProblems.rar
In this Lecture we will
Learn that when it comes to web application development there are multiple technologies available to choose from. There are open-source technologies like Java, PHP and closed source technologies like ASP.NET MVC.
Learn that ASP.NET Core , MVC's successor is now an open source , cross-platform framework developed by Microsoft. Basically it is a complete reform of ASP.NET that combines MVC structure and Web API into a single framework.
Look at the elements that make ASP.NET Core the right choice for app development
The MVC architecture provides a clear separation of concerns.
Razor Pages is a new element of ASP.NET Core that makes programming page-focused scenarios more productive. As you know from our previous section when you work with the MVC framework , the controller classes are filled with a large amount of actions. And not only that, but they also grow as the new things are added. With Razor pages, each web page becomes self-contained with its View (.cshtml Razor file) component and a .cshtml code behind file.
Provides support for popular Javascript frameworks.
NET core framework provides build-in templates for two of the most popular frameworks, Angular and React.
Improved Collaboration and Cross - Platform support. This means the apps built using this framework can run on Windows, Linux, and Mac operating systems.
Look at the key advantages of Using Razor Pages
Razor pages are better organized. Files are basically more organized. You have a Razor View and the entire code behind file, the same way the old ASP.NET Webforms did.
Single Responsibility. In Razor Pages, each app page is self contained with its own view and code organized together, which is less complex than MVC
ASP.NET is a Modular Web Framework. Everything is managed using the NuGet package, which means it is easier than MVC to upgrade the existing framework without releasing new .Net Framework versions every time new things are added.
In this Lecture we will
Create our first Razor Pages web app (RaptorsCoreRazor/RaptorsCoreRazorNET7)
From the Visual Studio File menu, select New -> Project
Create a new ASP.NET Core Web Application and select Next
Name the project RaptorsCoreRazor
Select ASP.NET Core 6/7/8 in the dropdown Web Application and then select Create.
Run the app
Examine the project files
Let's focus on the the Pages folder
This folder contains Razor pages and supporting files
Each Razor page is a pair of files
A .cshtml file that contains HTML markup with C# code using Razor syntax.
A .cshtml.cs file that contains C# code that handles page events.
Supporting files have names that begin with an underscore.
_Layout.cshtml configures UI elements common to all pages. This file sets up the navigation menu at the top of the page
wwwroot folder
Contains static files such as the HTML files, JavaScript files and CSS files.
appSettings.json
Contains configuration data such as connection strings.
Program.cs
Contains the entry point for the program.
Startup.cs
Contains code that configures app behavior such as whether it requires consent for cookies.
In this Lecture we will
Add a data model
we add a class for managing basketball player info in a database.
these classes are used with Entity Framework Core to work with a database
first we add a folder called Models
then we right click the Models folder ... Select Add -> Class and name the class Raptor
add the following properties to this new class
public class Raptor
{
//ID field is required by the database for the primary key
public int Id { get; set; }
public int PlayerNum { get; set; }
public string PlayerName { get; set; }
public string PlayerPosition { get; set; }
public string PlayerHeight { get; set; }
public double PlayerSalary { get; set; }
}
recall the ID field is required by the database for the primary key
Scaffold the Raptor model
the scaffolding tool produces pages for Create, Read, Update and Delete (CRUD) operations for the Raptor model
create a Pages/Raptors folder
right click on the Pages/Raptors folder ... Add -> New Scaffolded Item
in the Add Scaffold dialog select Razor Pages using Entity Framework (CRUD) -> Add
complete the Add Razor Pages using Entity Framework (CRUD) dialog
in the Model class drop down select Raptor (RaptorsCoreRazor.Models)
in the Data context class row, select the + sign and accept the generated name
select Add
The appsettings.json file is updated with the connection string used to connect to a local database
the scaffold process creates and updates the following files
Pages/Raptors: Create, Delete,Details,Edit and Index
Data/RaptorsCoreRazorContext.cs
Startup.cs is updated
Perform the initial migration
In this section the Package Manager Console is used to
add an initial migration
update the database with the initial migration
From the Tools menu, select NuGet Package Manager -> Package Manager Console
in the PMC enter ther following command
Add-Migration Initial
Update-Database
Test the app
run the app and append /Raptors to the URL in the browser
test out the Create link
test out the Edit,Details and Delete links
In this Lecture we will
First we examine the Pages/Raptors/Index.cshtml.cs page
public class IndexModel : PageModel
{
private readonly RaptorsCoreRazor.Models.RaptorsCoreRazorContext _context;
public IndexModel(RaptorsCoreRazor.Models.RaptorsCoreRazorContext context)
{
_context = context;
}
public IList<Raptor> Raptor { get;set; }
//When a request is made for this page the OnGetAsync method returns
//a list of players to the Razor page.
public async Task OnGetAsync()
{
Raptor = await _context.Raptor.ToListAsync();
}
}
... and the associated Razor Page
@page
@*The @page Razor directive makes the file into an MVC action
which means that it can handle requests.
@page must be the first Razor directive on a page*@
@model RaptorsCoreRazor.Pages.Raptors.IndexModel
@*The @model directive specifies the type ofmodel passed to the Razor
page.*@
@{
ViewData["Title"] = "Index";
}
@*You add objects into the ViewData Dictionary using a key/value pattern
In the preceding sample the Title property is added to the ViewData dictionary
The Title property is used in the Pages/Shared/_Layout.cshtml file*@
<h1>Index</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Raptor[0].PlayerNum)
</th>
<th>
@Html.DisplayNameFor(model => model.Raptor[0].PlayerName)
</th>
<th>
@Html.DisplayNameFor(model => model.Raptor[0].PlayerPosition)
</th>
<th>
@Html.DisplayNameFor(model => model.Raptor[0].PlayerHeight)
</th>
<th>
@Html.DisplayNameFor(model => model.Raptor[0].PlayerSalary)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Raptor) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.PlayerNum)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlayerName)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlayerPosition)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlayerHeight)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlayerSalary)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Next we take a deeper look at the layout page
Click each of the links in the Index page including Home and Privacy. Each page show the same menu layout. The menu layout is implemented in the Pages/Shared/_Layout.cshtml file.
this layout template allows you to specify the HTML container layout of your site in one place and then apply it across multiple pages in your site.
find the @RenderBody() line. RenderBody is a placeholder where all the page-specific views you create show up wrapped in the layout page.
Let's update a few items in the layout
change the <title> element to display "Toronto Raptors"
update the following anchor element
<a class="navbar-brand" asp-area="" asp-page="/Raptors/Index">Player Info</a>
Save your changes and test the app by clicking on Player Info
In this Lecture we will
Learn to access the SQL Server Express LocalDB
LocalDB is a lightweight version of the SQL Server Express database engine that's targeted for program development.
From the View menu open SQL Server Object Explorer
Click inside the LocalDB folder and then the Databases folder and find the MVCBasketballContext folder
Open up the Tables folder and then right click on dbo.Raptors and select View Designer
Note the key icon next to ID. By default, EF creates a property names ID for the primary key.
Right click dbo.Raptors again and select View Data (clear out all the data)
Learn how to initially seed the database
First we create a new class named SeedData in the Models folder with the following code:
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new RaptorsCoreRazorContext(
serviceProvider.GetRequiredService<
DbContextOptions<RaptorsCoreRazorContext>>()))
{
// Look for any player info
if (context.Raptor.Any())
{
return; // DB has been seeded
}
context.Raptor.AddRange(
new Raptor
{
PlayerNum = 9,
PlayerName = "Serge Ibaka",
PlayerPosition = "Forward",
PlayerHeight = "7-0",
PlayerSalary = 22271604
},
new Raptor
{
PlayerNum = 33,
PlayerName = "Marc Gasol",
PlayerPosition = "Centre",
PlayerHeight = "6-11",
PlayerSalary = 25595700
},
new Raptor
{
PlayerNum = 7,
PlayerName = "Kyle Lowry",
PlayerPosition = "Guard",
PlayerHeight = "6-0",
PlayerSalary = 34996296
},
new Raptor
{
PlayerNum = 43,
PlayerName = "Pascal Siakam",
PlayerPosition = "Forward",
PlayerHeight = "6-9",
PlayerSalary = 2351839
},
new Raptor
{
PlayerNum = 23,
PlayerName = "Fred VanVleet",
PlayerPosition = "Guard",
PlayerHeight = "6-1",
PlayerSalary = 9346153
},
new Raptor
{
PlayerNum = 24,
PlayerName = "Norman Powell",
PlayerPosition = "Guard",
PlayerHeight = "6-3",
PlayerSalary = 10116576
},
new Raptor
{
PlayerNum = 3,
PlayerName = "OG Anunoby",
PlayerPosition = "Forward",
PlayerHeight = "6-7",
PlayerSalary = 2281800
}
);
context.SaveChanges();
}
}
Note: If there are any players in the DB, the seed initializer returns and no players are added.
Next we must add a seed initializer
in Program.cs we modify the Main method to do the following
Get a DB context instance from the dependency injection container
Call the seed method passing to it the context
Dispose the context when the seed method completes
public static void Main(string[] args)
{
//Here we start the process of adding the seed initializer
var host = CreateWebHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<RaptorsCoreRazorContext>();
context.Database.Migrate();
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
Test the app
In this Lecture we will
Learn that the scaffolded Basketball app has a good start, but the presentation isn't Ideal. For instance PlayerNum should be Player Number when displayed in the label.
Update the generated code in the Raptor.cs file
public class Raptor
{
//ID field is required by the database for the primary key
public int Id { get; set; }
[Display(Name = "Player Number")]
public int PlayerNum { get; set; }
[Display(Name = "Player Name")]
public string PlayerName { get; set; }
[Display(Name = "Position ")]
public string PlayerPosition { get; set; }
[Display(Name = "Height")]
public string PlayerHeight { get; set; }
[Display(Name = "Salary")]
[Column(TypeName = "double(18,2)")]
public double PlayerSalary { get; set; }
}
The [Column(TypeName="double(18,2)"] data annotation enables Entity Framework Core to correctly map Salary to currency in the database.
The Display attribute specifies what to display for the name of a field.
Review the concept of Posting and Binding in the Pages/Raptors/Edit.cshtml.cs file
public class EditModel : PageModel
{
private readonly RaptorsCoreRazor.Models.RaptorsCoreRazorContext _context;
public EditModel(RaptorsCoreRazor.Models.RaptorsCoreRazorContext context)
{
_context = context;
}
[BindProperty]
public Raptor Raptor { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Raptor = await _context.Raptor.FirstOrDefaultAsync(m => m.Id == id);
if (Raptor == null)
{
return NotFound();
}
return Page();
}
//This code detects concurrency exceptions when the one client deletes
//the player and the other client posts changes to the player info
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Attach(Raptor).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!RaptorExists(Raptor.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
private bool RaptorExists(int id)
{
return _context.Raptor.Any(e => e.Id == id);
}
}
When an HTTP GET request is made to the Raptors/Edit page for example http://localhost:500/Raptors/Edit/2
The OnGetAsync method fetches the player info from the database and returns the Page method
The Page method renders the Pages/Raptors/Edit.cshtml Razor Page
The Pages/Raptors/Edit.cshtml file contains the model directive (@model RaptorsCoreRazor.Pages.Raptors.EditModel), which makes the Raptor model available on the page.
The Edit form is displayed with the values from the Player Info
When the Raptors/Edit page is posted
The form values on the page are bound to the Raptor property
The [BindProperty] attribute enables Model binding
In this Lecture we will
Learn how to implement at search by "Position" in our Basketball web app
First we need to update Pages/Raptors/Index.cshtml.cs
public class IndexModel : PageModel
{
private readonly RaptorsCoreRazor.Models.RaptorsCoreRazorContext _context;
public IndexModel(RaptorsCoreRazor.Models.RaptorsCoreRazorContext context)
{
_context = context;
}
public IList<Raptor> Raptor { get;set; }
//SearchString contains the text users enter into the search text box
//[BindProperty] binds form values and query strings with the same
//name as the property
//SupportsGet = true is required for binding on GET requests
[BindProperty(SupportsGet =true)]
public string SearchString { get; set; }
//When a request is made for this page the OnGetAsync method returns
//a list of players to the Razor page.
public async Task OnGetAsync()
{
var pos = from m in _context.Raptor
select m;
if (!string.IsNullOrEmpty(SearchString))
{
pos = pos.Where(s => s.PlayerPosition.Contains(SearchString));
}
Raptor = await pos.ToListAsync();
}
}
Recall the s=>s.PlayerPosition.Contains() code is a Lambda Expression. Lambdas are used in method-based LINQ queries as arguments to standard query operator methods such as the Where method or Contains.
Also note that the Contains method is run on the database, not in the C# code.
Test out the Search
Navigate to the Raptors page and append a query string such as ?searchString=guards to the URL
However you can't expect users to modify the URL to search for a player.
Implement a UI component to the Razor Page for the Index so that a user can enter a search query into a text box.
Open the Pages/Raptors/Index.cshtml file and add the <form> markup below
<p>
<a asp-page="Create">Create New</a>
</p>
@*When the form is submitted, the filter string is sent to the
Pages/Raptors/Index page via query string
Here we are using the input tag helper with its one attribute for
... An expression to be evaluated against the current
PageModel usually a PageModel property name ... SearchString*@
<form>
<p>
Title: <input type="text" asp-for="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>
In this Lecture we will
Learn how to add a new property called "PlayerCollege" to the Raptor model
First we open Models/Raptor.cs and add the PlayerCollege property
public class Raptor
{
//ID field is required by the database for the primary key
public int Id { get; set; }
[Display(Name = "Player Number")]
public int PlayerNum { get; set; }
[Display(Name = "Player Name")]
public string PlayerName { get; set; }
[Display(Name = "Position ")]
public string PlayerPosition { get; set; }
[Display(Name = "Height")]
public string PlayerHeight { get; set; }
[Display(Name = "Salary")]
[Column(TypeName = "double(18,2)")]
public double PlayerSalary { get; set; }
[Display(Name = "College")]
public string PlayerCollege { get; set; }
}
Next we open Pages/Raptors/Index.cshtml and add the PlayerCollege field
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Raptor[0].PlayerNum)
</th>
<th>
@Html.DisplayNameFor(model => model.Raptor[0].PlayerName)
</th>
<th>
@Html.DisplayNameFor(model => model.Raptor[0].PlayerPosition)
</th>
<th>
@Html.DisplayNameFor(model => model.Raptor[0].PlayerHeight)
</th>
<th>
@Html.DisplayNameFor(model => model.Raptor[0].PlayerSalary)
</th>
<th>
@Html.DisplayNameFor(model => model.Raptor[0].PlayerCollege)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Raptor) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.PlayerNum)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlayerName)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlayerPosition)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlayerHeight)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlayerSalary)
</td>
<td>
@Html.DisplayFor(modelItem => item.PlayerCollege)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
</td>
@*Tag helpers enable server-ise code to participate in creating and rendering HTML
elements in Razor files.
In the preceding code the AnchorTagHelper dynamically generates
the HTML href attribute value from the Razor Page*@
</tr>
}
</tbody>
</table>
You will also need to update the Delete and Details , Edit and Create pages with the PlayerCollege field
Learn that the app won't work until the DB is updated to include the new field. If run now, the app throws a SqlException
We will resolve this error manually
We will explicitly modify the schema of the existing database so that it matches the model classes. The advantage of this approach is that you keep your data.
Finally run the app to confirm the addition of the new field
In this Lecture we will
Learn how to implement validation logic in the Raptor model. The validation rules are enforced any time a user creates or edits a player's info
Learn about the DRY tenet (Don't Repeat Yourself). Razor pages encourage development where functionality is specified once, and it's reflected throughout the app.
The validation support provided by Razor Pages and Entity Framework is a good example of the DRY principle. Validation rules are declaratively specified in one place (in the model class) and the rules are enforced everywhere in the app.
Add validation rules to the Raptor model
The DataAnnotations namespace provides a set of built-in validation attributes that are applied declaratively to a class or property. DataAnnotations also contains formatting attributes like DataType that help with formatting and don't provide any validation
Update the Raptor class to take advantage of the some the built-in validation attributes.
public class Raptor
{
//ID field is required by the database for the primary key
public int Id { get; set; }
[Range(1, 100)] //between 1 and 100
[Required] //must have a value
[Display(Name = "Player Number")]
public int PlayerNum { get; set; }
[StringLength(60, MinimumLength = 3)] //maximum and minimum length of string
[Required]
[Display(Name = "Player Name")]
public string PlayerName { get; set; }
[Display(Name = "Position ")]
[StringLength(60, MinimumLength = 3)]
[Required]
public string PlayerPosition { get; set; }
[Display(Name = "Height")]
[Required]
public string PlayerHeight { get; set; }
[Display(Name = "Salary")]
[Range(1, 100000000)]
[DataType(DataType.Currency)] //lots of other DataType attributes available
[Required]
public double PlayerSalary { get; set; }
[Display(Name = "College")]
[StringLength(60, MinimumLength = 3)]
public string PlayerCollege { get; set; }
}
Note: the DataType attributes do not provide any validation
Run the app , navigate to the Raptors page. Select ... Create New link. Complete the form with some invalid values. When jQuery client side validation detects the error, it displays an error message.
Notice how the form has automatically rendered a validation error message in each field containing an invalid value. The errors are enforced both client-side (using JavaScript and jQuery) and server-side (when a user has JavaScript disabled).
A significant benefit is that no code changes were necessary in the Create or Edit pages. Once DataAnnotations were applied to the model, the validation UI was enabled. The Razor Pages created in this tutorial automatically picked up the validation rules (using validation attributes on the properties of the Raptor model class). Test validation using the Edit page, the same validation is applied.
The form data isn't posted to the server until there are no client-side validation errors.
In this Lecture we will
Discuss your next steps
In this Lecture we will
Learn how we develop web applications today
Discuss how we can use C# for both server-side and client side developement
Discuss how Blazor is a feature of ASP.NET for building interactive web UIs using C# instead of JavaScript.
Blazor is a framework that adds client-side interactivity to web applications with .NET. In Blazor, developers use C# codes and Razor syntaxes to create client-side features without the need to use JavaScript at all. This is a big plus point for developers who do not know JavaScript.
Blazor embraces the Single Page Application (SPA) architecture which rewrites the same page dynamically over a persistent HTTP connection (SignalR) maintained with the Server.
There is no page reload when the Blazor code communicates with the server. Everything is done asynchronously just like AJAX.
Discuss how a browser can execute C# code using WebAssembly
Recap the different run-time modes Server Side and Client Side Blazor (WebAssembly)
In this Lecture we will
Take a deeper look at how Blazor offers 2 hosting models Blazor WebAssembly and Blazor Server
The WebAssembly Hosting Model
Benefits
Downsides
The Blazor Server hosting Model
Benefits
Downsides
Create the default Blazor Server and WebAssembly applications
In this Lecture we will
Highlight the differences and similarities between the Blazor Server project structure and the WebAssembly project structure
program.cs
wwwroot
app.razor
Pages folder
Shared folder
MainLayout.razor
NavMenu.razor
_Import.razor
wwwroot/index.html (WebAssembly)
startup.cs (Server)
pages/_Host.cshtml (Server)
Data folder (Server)
appsettings.json (Server)
Blazor is known as a SPA framework
The term Single-Page Application (SPA) seems a bit weird when you open the app and start navigating from one page to another. However, technically SPAs consist of a single HTML page and Blazor or any other JavaScript framework (ReactJS, Angular, etc.) dynamically updates the content of that page. As you have seen, in the wwwroot folder, there is only one HTML page in our whole application, called index.html.
In this Lecture we will
Learn to re-use and edit a Blazor component
Component files have the extension .razor and must start with a capital letter
Razor files are a combination of HTML markup defining the user interface and the C# code defining the processing logic ... not two separate files as we have learned with Razor pages
ComponentsIntroPart1 (Blazor Server App)
@page "/counter"
@*One way to have the Counter component rendered is by navigating to /counter
in the browser. This path is specified by the @page directive at the top
of the component.
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
ComponentsIntroPart2 (Blazor Server App)
Here we reference the Counter component in the Index.razor page
@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.
@*Here we are nesting the Counter component*@
<Counter />
<Counter />
<SurveyPrompt Title="How is Blazor working for you?" />
Learn to create and implement a simple component
ComponentsIntroPart3 (Blazor Server App)
@page "/componentexample"
<h3 style="background-color:goldenrod">This is my first Component Example</h3>
@code {
}
In this Lecture we will
Look at how to split a razor component into two files with the HTML in one file and the C# code in a separate file
Discuss how this technique is good practice from a maintenance and unit testing standpoint
SplitComponents
Counter.razor
@page "/counter"
A component is a combination of HTML markup and C# code in one file
These can also be separate files ... partial files*@
<h1>Counter</h1>
@*Adding a style and a reference to the fontFamily variable *@
<p style="font-family:@fontFamily ">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
Counter.razor.cs
namespace ComponentsIntroPart1.Pages
{
//class changed to partial class
//file has same name but .cs after .razor
public partial class Counter
{
private int currentCount = 0;
private string fontFamily = "Verdana";
private void IncrementCount()
{
currentCount++;
}
}
In this Lecture we will
Learn that while ASP.NET Web Forms and Blazor have many similar concepts there are differences in how they work
Examine the inner workings and architectures of ASP.NET Web Forms and Blazor
ASP.NET Web Forms
page-centric
pages consist of HTML, C#, Code behind class containing logic and event handling and controls
pages are files that end with .aspx containing markup, controls and some code
code behind classes are in files with the same base name and .asps.cs extension
controls on a page typically post-back to the same page that presented the control and carry along with them a payload from a hidden form field called ViewState.
Blazor
client side web UI framework similar in nature to Javascript front-end frameworks like Angular or React
handles user interactions and renders the necessary UI updates
apps consist of one or more components that are rendered on an HTML page
components are .NET classes that represent a reusable piece of UI.
each component maintains its own state and specifies its own rendering logic
after a component handles an event, Blazor renders the component and keeps track of what changed in the rendered output.
the components don't render directly to the DOM. They instead render to an in-memory representation of the DOM called a RenderTree so that Blazor can track the changes.
Blazor compares the newly rendered output with the previous output to calculate a UI diff that it then applies to the DOM
In this Lecture we will
Learn that Blazor components can pass values to other components using parameters
Learn that component parameters are defined using the [Parameter] attribute
Discuss the use of parameters in the Blazor default app
ParametersIntroPart1SeeIndexSurvey
Create a simple implementation of parameters
ParametersIntroPart2
Create a razor component ParameterExampleComponent.razor
<h3>Parameter Example Component</h3>
<h5 style="color:red">@Title</h5>
@*Razor components can pass values to other components using
parameters (we are going to call/implement this component in index.razor)
Component parameters are defined using the [Parameter] attribute
which must be declared as public*@
@code {
[Parameter]
public string Title { get; set; }
//Here we are using an auto-implemented property
}
Then implement the parameter in Index.razor
@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.
@*Here we are nesting the Counter component*@
<Counter />
<Counter />
<SurveyPrompt Title="How is Blazor working for you?" />
<ParameterExampleComponent Title="Passed from Parent" />
ParametersIntroPart3 (Implementing OnParametersSet() )
We will implement a parameter in the Counter page and then refer to it in the Index page ... <Counter Increment="10" />
We will add a parameter to specify the increment used by the IncrementCount() method
@code {
private int currentCount = 0;
private string fontFamily = "Verdana";
//new variable which will allow us to increment by any amount via a parameter ...
//if no value is specified (null) it will default to 1
private int increment = 1;
[Parameter]
public int? Increment {get;set;}
//? is shorthand for Nullable<int> (a nullable type) ...this allows you to have nullables for Increment parameter
//Now need a way to get Increment (public) coming in from the 'outside' ie. <Counter Increment="10" />
//Into our private variable increment
protected override void OnParametersSet()
{
if (Increment.HasValue)
{
increment = Increment.Value;
}
}
private void IncrementCount()
{
//currentCount++;
//now we can go up by whatever we want ... or the default 1
currentCount += increment;
}
}
Learn that so far we’ve seen how to link a static URL to a Blazor component. Static URLs are only useful for static content, if we want the same component to render different views based on information in the URL (such as a customer id) then we need to use route parameters. (RouteParameters)
A route parameter is defined in the URL by wrapping its name in a pair of { braces } when adding a component’s @page declaration.
@page "/routepage/{CustomerId?}"
The “?” at the end of route parameter specifies that the parameter accepts null value. This is the case when no value is passed for the route parameter
@page "/routepage/{CustomerId?}"
<h3>Route Parameters Example</h3>
@if (string.IsNullOrEmpty(CustomerId))
{
<h4>No customer entered</h4>
}
else
{
<h4 class="bg-info text-white">Hello customer @CustomerId</h4>
}
@code {
[Parameter]
public string CustomerId { get; set; }
}
To test out ...
1) Run app and just click on Route Parameters link in the NavMenu first ... then
2) Run app and hand type url routepage/2345
Supplementary Demos
BookAppClassParameterLect90
This is a simple implementation of Components with Parameters where the BookCard Component is used to display the details of a Book Object (an instance of the Book class ... located in the Models folder).
In this Lecture we will
Learn that binding data is a fundamental task in single page applications. At some point every application needs to either display data (eg labels) or receive data (eg. forms)
Learn about the two types of binding
One Way ... means that updates to the value only flow one way, for instance rendering a label... the user will never be able to modify the value directly. So you set or alter a value in one part of your code and have it display in another part of your code.
Two Way ... means bindings have a bidirectional flow... ie they allow values to be updated from two directions ... the primary use for two way binding is in forms
Create a simple application which illustrates One and Two Way Binding
DataBindingIntro
@page "/OneWayBinding"
<h3>@Title</h3>
@code {
string Title = "This is a simple One Way Binding";
}
@page "/TwoWayBinding"
<h3>Two Way Binding</h3>
@*When we run the app initially and navigate to this
page we see the SliderValue set to zero using
one way binding*@
<p> Slider Value: <b>@SliderValue</b> </p>
@*Here we add a slider control that is bound to the
SliderValue variable
This time we are using the @bind-value attribute and
an event parameter oninput to activate two-way binding
When we move the slider the SliderValue variable is
updated immediately and displayed
When we manipulate a variable property through
the UI control we are implementing two way binding
because the consumer is updating the source*@
<input type="range" step="1"
@bind-value="SliderValue"
@bind-value:event="oninput" />
@code {
int SliderValue = 0;
}
Supplementary Demo
DataBindIntroExtraExNET6
incorporates a class called DemoProduct
incorporates a checkbox
<input type="checkbox" @bind-value="@Product.IsActive" checked="@(Product.IsActive? "checked":null)" />
In this Lecture we will
Create a simple Blazor application to demonstrate One Way and Two Way Binding
Colours (WebAssembly App)
Incorporates ... loops (foreach), if statements, C# Lists,Bootstrap classes
@page "/oneway"
@*One way binding is used for printing values in your views
These values could be strings such as a title in a
<h1> tag like below or items in a list.
But it can also be used to dynamically inject values into the
HMTL such as the name of a CSS class on an element*@
<h1>@Title</h1>
Welcome to my one-way binding example. Here's a list of colours.
<ul>
@foreach (var colour in Colours)
{
<li>@colour</li>
}
</ul>
@if (DateTime.Now.DayOfWeek==DayOfWeek.Saturday || DateTime.Now.DayOfWeek==DayOfWeek.Sunday)
{
<p class="@weekendFontStyle"> It's the Weekend</p>
}
else
{
<p> It's @DateTime.Now.DayOfWeek.ToString()</p>
}
@code {
string Title = "Welcome to one-way binding";
List<string> Colours = new List<string> {
"Red","Blue","Green","Yellow"
};
string weekendFontStyle = "alert-warning";
}
@page "/twoway"
@*With one way binding you now know how to print out values
to a view, but what if you would like a user to be able
to update those values? This is where two-way binding comes in
Blazor uses a directive called bind to achieve this*@
<h1>Record your current favourite colour</h1>
<p>
<input @bind="Name"
@bind:event="oninput"/>
</p>
<p>
<select @bind="FavouriteColour">
<option>Red</option>
<option>Blue</option>
<option>Green</option>
<option>Yellow</option>
</select>
</p>
<p>Hi @Name</p>
<p>Your favourite colour is @FavouriteColour </p>
@code {
public string Name { get; set; }
public string FavouriteColour { get; set; }
}
Offer you an Exercise to review and extend your skills
DataBindingPart2Exercise/DataBindingPart2ExerciseSolution
Implements Lists, DropDownLists, and Bootstrap classes
Supplementary Demo
Lecture92ExtraExampleOneVsTwoWayBind
clearly demos difference between one and two way binding
In this Lecture we will
Create another Blazor application to further demonstrate One Way and Two Way Binding
UnitsConverter (WebAssembly App)
Before Binding ... using html input value property and the onchange event
@page "/OneWayBindingConverter"
<h3>Converting Inches to Centimeters</h3>
@*One Way Binding with an Event*@
@*initial simple interface without event handler
<input value=@inches type="number" />*@
@*Here we are using the input's value property
and manually handling the onchange event
however there is an easier way when we look at
two way binding*@
<input value="@inches"
@onchange="UpdateValue"
type="number" />
<p>The value of inches is : @inches</p>
@code {
double inches = 1;//default value;
void UpdateValue(ChangeEventArgs e)
{
double val = 0;//Failing to parse will set to 0
double.TryParse(e.Value.ToString(), out val);
inches = val;
}
}
With Two Way Binding ... binding uses the onchange event by default but in this implementation we use the oninput bind event which occurs immediately as we type and also incorporate Properties to perform the actual conversions
<h4>Advanced Two-Way Binding using oninput</h4>
<label>Inches</label>
<input type="number" @bind="Inches"
@bind:event="oninput"/>
<span>=</span>
<input type="number" @bind="Centimeters"
@bind:event="oninput"/>
<label>Centimeters</label>
@code {
double inches = 1;//default value
double centimeters = 2.54;//default value
//We are going to add properties called Inches and Centimeters
public double Inches
{
//get => inches;
get
{
return inches;
}
set
{
centimeters = value * 2.54;//both inches and
inches = value; //centimeters are updated
}
//when the values are updated by the set method
//data binding will ensure the changes are reflected
//in the UI
}
public double Centimeters
{
//get => centimeters;
get
{
return centimeters;
}
set
{
inches = value / 2.54;
centimeters = value;
}
}
Offer you the challenge of the Currency Exchange Problem (CurrencyExchange)
Offer you the challenge of the Birthday Problem (BirthdayProblem)
Note use of DateTime.TryParse(DateOfBirth, out var date)
DayOfWeek = date.DayOfWeek.ToString()
Supplementary Demos
MoreDataBinding
Focus on the Counter page where we alternate the colour of the number being updated between red and yellow.
Note the use of a style classes ... stored in app.css in wwwroot folder
.red-background {
background:red;
color:white;
}
.yellow-background {
background:yellow;
color:black;
}
Note the use of the ternary operator in the IncrementCount() method to toggle back and forth between the red and yellow backgrounds
Notice how we disable the Click Me button when the currentCount goes over 10
MoreDataBindingKeypress
Here we react to the keypress event connecting to the KeyHandler method where pressing + and - will increment and decrement the value in the input ... but initially you will also see the key you just pressed (ie +) added to the input html element because this is the default behavior. To stop this default behavior we add preventDefault
<input type="number" @bind="@increment"
@onkeypress="KeyHandler"
@onkeypress:preventDefault="true"/>
@increment
@code {
private void KeyHandler (KeyboardEventArgs e)
{
if (e.Key=="+")
{
increment += 1;
}
if (e.Key=="-")
{
increment -= 1;
}
}
}
BindTimeFormatsCultureSpecific
The value of the DateTime can be formatted according to your needs.
<input type="text" class="form-control" @bind="BirthDate" @bind:format="yyyy-MM-dd"/>
The values of the DateTime can be shown in a culture specific format
@using System.Globalization
@*necessary for CultureInfo reference in code section*@
@*In the next code I am specifying the culture through @bind:culture attribute.
There are 2 text boxes that show appointment times in fr-FR (French) and en-gb (Great Britian) cultures*@
<input type="text" class="form-control" @bind="ApptTime" @bind:format="MMM-dd" @bind:culture="CultureGB" />
<input type="text" class="form-control" @bind="ApptTime" @bind:format="MMM-dd" @bind:culture="CultureFR" />
@code {
private DateTime BirthDate = new DateTime(1959, 5, 24);
public DateTime ApptTime { get; set; } = DateTime.Parse("1959/04/24");
public CultureInfo CultureGB { get; set; } = CultureInfo.GetCultureInfo("en-GB");
public CultureInfo CultureFR { get; set; } = CultureInfo.GetCultureInfo("fr-FR");
//Go here for more info on CultureInfo
//https://csharp.net-tutorials.com/working-with-culture-and-regions/the-cultureinfo-class/
}
In this Lecture we will
Learn to implement State Management in Blazor
State Management refers to the technique that you use to persist data between Blazor pages. Without state management this data would be lost.
First we look at the issue ... state is not maintained
we use the default server-side Blazor app and add the Counter component to the Index page
BlazorStateManagePt1
We introduce a CounterState class with a public counter to implement state management
Here we use Dependency Injection which is the best way to pass data between Blazor pages .
We register a singleton service and inject it as a dependency onto the pages or components that need it.
CounterState.cs
public class CounterState
{
public int CurrentCount { get; set; }
}
BlazorStateManagePt2 (Pre .NET 6)
Startup.cs
services.AddScoped<CounterState>();
BlazorStateManagePt2net6 (.NET 6 +)
Program.cs
builder.Services.AddSingleton<CounterState>();
BlazorStateManagePt2net6Updated
@page "/counter"
@inject BlazorStateManagePt2net6.SessionState.CounterState CounterState
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
@*State Management update*@
<p>Current count: @CounterState.CurrentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private void IncrementCount()
{
//Session State object
CounterState.CurrentCount++;
}
}
Supplementary Demo
DependencyInjectionIntroWASM1
This application serves as a Review of some of the State Management skills covered in Lecture 94
State Management is one of the much needed features in modern web apps ... recall the issue when we move between various pages (ie Counter page) ... we don't want to lose values of fields and properties '
With State Management the data is managed in the browser using an in memory state container service and frequent postbacks to the server can be avoided.
The best way to pass data between Blazor pages is by registering a singleton service and injecting it as a dependency onto the pages or components that need it. In this demo, we create a C# class, register it as a service in your Blazor app, inject an instance of the service onto your pages, and use the service to share data across Blazor components.
This technique you learn in this demo will allow you to pass parameters from one Blazor component to another, even if those components are on different pages. This is not only important for keeping your C# code clean, but it is also ensures the end user has a consistent experience across all pages of your site. Moreover, using a service class enables you to you to pass data in a way that does not require the use of routing parameters.
The Steps
Initial Setup ... Create a folder called Services and add a new C# class called AppData.cs (public int Age) Initial Setup ... In the Pages folder create a new Razor Components called Page1.razor and Page2.razor
Our objective is to create a simple Web App where you set the value of a property on one page and another page will be aware of the value
Next we must register our AppData.cs class as a Service by adding a reference to it in the Program.cs file... builder.Services.AddSingleton();
Here we have added the AppData class (from the Services folder) with a Singleton lifetime. Singleton is perfect for a client side Blazor app, but if you are working with Server-Side Blazor you will want to register as a Scoped service so each different user receives his/her own instance of the service for the duration of their session.
A Singleton allows the same instance of a service class to be shared across components. This is good, because you want to be sure all your pages receive the same instance of the AppData service class. This will ensure that the data (namely Age, in this example) is correctly shared between pages throughout the lifetime of the application.
We use two pages (Page1/Page2) for our demo. We inject the singleton instance of the AppData service into both pages. Note the UI is refreshed when the bind:event is raised.
In this Lecture we will
Revisit creating components and how to pass parameters to them
Products1
Index.razor
@page "/"
@*original content removed and replaced by
reference to newly created razor component called Home
<Home></Home>
... then further modified with a parameter *@
<Home Title="Welcome to the Products Page"></Home>
Home.razor
@page "/home"
<div style="text-align:center">
<h1>
@*Welcome to the Products Page
replaced by parameter variable below*@
@*Here we are using one-way binding to bind the
value from the Title property from the code section
below*@
@Title
</h1>
<p>
Feel free to browse
</p>
<p>
<img src="images/krispies.jpeg" width="300" height="400" class="img-fluid" />
</p>
</div>
@code {
//If we want to mark any property as a component parameter
//we have to decorate it with the [Parameter] attribute
//This allows us to create reusable components that accept
//different parameters and show content based on those parameters
[Parameter]
public string Title { get; set; }
}
Learn that in addition to sending parameters to the component we can send content as well. This is very useful when we have HTML markup that we want to use inside the component. We do this by using a RenderFragment parameter. It is a bad solution trying to send HTML code through the regular parameters to the component because it would be hard to maintain and it is not easily readable.
Products2
Review how to separate our main razor file (Home) into two separate (but still connected) files by creating a Partial class
Implement the RenderFragment inside the Partial class
[Parameter]
public RenderFragment BrowseContent { get; set; }
Then we modify the Home component file
@page "/home"
<div style="text-align:center">
<h1>
@Title
</h1>
<p>
@*Feel free to browse ...
has been replaced by the newly created property*@
@BrowseContent
</p>
<p>
<img src="images/krispies.jpeg" width="300" height="400" class="img-fluid" />
</p>
</div>
And finally we modify the Index file
@page "/"
<Home Title="Welcome to the Products Page">
@*Here we explicitly specify the name of the
RenderFragment property*@
<BrowseContent>
<b>Feel free to browse</b>
</BrowseContent>
</Home>
Supplementary Demos
MoreComponentsAndParameters
uses a boolean Parameter to hide or show a RenderFragment Parameter (a button on the Index page toggles the ChildContents on and off)
Alert.razor
@if(Show)
{
<div class="alert alert-secondary mt-4" role="alert">
@ChildContent
</div>
}
@code{
[Parameter]
public bool Show { get; set; }
@* The ChildContent can hold complex content (HTML markup for instance)
and therefore needs to be of type RenderFragment *@
[Parameter]
public RenderFragment ChildContent { get; set; }
}
Index.razor
@page "/"
@* Quick simple review of the use of a RenderFragment Parameter *@
<Alert Show="@ShowAlert">
<ChildContent>
<span class="oi-check mr-2" aria-hidden="true"></span>
<strong>Blazor Components and RenderFragment Example</strong>
</ChildContent>
</Alert>
<button @onclick="ToggleAlert">Toggle</button>
@code {
public bool ShowAlert { get; set; } = true;
public void ToggleAlert()
{
ShowAlert = !ShowAlert;
}
}
Lecture95ExtraReviewExampleDashboard (additional enrichment example ... using embedded components)
This application serves as a Review and Extension of some basic Blazor concepts ... including
Components...Components...Components
Bootstrap CSS ... Card class in particular (See Resource Link for more info)
Parameters
RenderFragments
Partial Files (Splitting Components)... localized css file
Click on the Dashboard link on the left to try out the Application
Click on the App Details/Help button on the Dashboard page for more coding details and concept reviews
Take a deeper dive by looking through the relevant razor component pages, they are fully documented
Provide you with some Supplementary Demos on the concept of Cascading Values and Parameters (CascadingValuesParameters ... CascadingIntro1/CascadingIntro2)
Cascading values and parameters provide a convenient way to flow data down a component hierarchy from an ancestor component to any number of descendent components. Unlike Component parameters, cascading values and parameters don't require an attribute assignment for each descendent component where the data is consumed.
CascadingIntro1
This application serves as an introduction to the Concept of Cascading Values and Parameters
One way to pass data from Parent component to child component is by using component parameters ... as we have shown in this Lecture
Components can also be nested. A component can be nested in another component. That component can be nested in yet another component and this can go on.
However, when there are several component layers in the component hierarchy, it's tedious to pass data from an ancestor component to a descendent component using component parameters. This is when we use cascading values and parameters. They provide a convenient way for an ancestor component to pass a value to all of its descendent components.
Blazor has a built-in component called CascadingValue. We can pass a value to this component. This value is then cascaded down its component tree to all of its descendants.
The child component can access the cascading value by declaring a property of the same type, decorated with the [CascadingParameter] attribute.
In fact, any of the descendant components in the component tree can access the cascading value. They simply have to declare a property of the same type, decorated with the [CascadingParameter] attribute.
Check out the ParentComponent.razor page in the Pages folder
Note how we have embedded the ChildComponent in the ParentComponent and how the GrandChildComponent is embedded inside the ChildComponent
Just like component parameters, if a cascading value is changed the change will be passed down to all descendants. And any components using the value will be updated and automatically have StateHasChanged called.
@page "/parentcomponent"
<h1 style="@Style">Parent Component Text</h1>
<CascadingValue Value="@Style">
<ChildComponent/>
</CascadingValue>
@code {
public string Style { get; set; } = "color:red";
}
ChildComponent.razor
<h3 style="@ElementStyle">Child Component</h3>
<GrandChildComponent/>
@code {
//We don't need to use the same name we
//used in the ParentComponent
[CascadingParameter]
public string ElementStyle{ get; set; }
}
GrandChildComponent.razor
<h5 style="@ElementStyle">GrandChild Component</h5>
@code {
[CascadingParameter]
public string ElementStyle { get; set; }
}
CascadingIntro2
In this 2nd version we are implementing Multiple Cascading Parameters
There are two ways of a dealing with this situation
The first is provided by the framework and is based on types. Say we had two cascading values, one is a string and one is an int. And there is a single child component.
Blazor will look at the type of the parameter and try and find a cascading value which matches
... But what happens if both cascading values have the same type ... See Cascading 3
In that situation, the framework is still going to match based on type. Except it will use the closest ancestor to the component requesting the parameter... in this case Name
The second, and most reliable way to identify cascading parameters is by name. When you create a cascading value you have the option to give it a name. Then when a child component wants to use it, they can ask for it by specifically.
@page "/parentcomponent4"
<h1 style="@Style">Parent Component Text ... Name=@Name</h1>
<CascadingValue Value="@Style" Name="TeacherStyle">
<CascadingValue Value="@Name" Name="TeacherName">
<ChildComponent4 />
</CascadingValue>
</CascadingValue>
@code {
public string Style { get; set; } = "color:green";
public string Name { get; set; } = "Charlie Chiarelli";
}
<h3 style="@ElementStyle">Child Component Text... Name = @TheName</h3>
@code {
[CascadingParameter (Name ="TeacherStyle")]
public string ElementStyle { get; set; }
[CascadingParameter(Name ="TeacherName")]
public string TheName { get; set; }
}
In this Lecture we will
Revisit and extend our knowledge of Blazor routing and show how to enable navigation between different sections in our application using NavigationManager
Products3
Recall ... if we want to set up the route in a component we have to use the @page directive followed by the route itself .For the Index page the route is /, which means it is the starting point for our application (the root) ....BUT the component is not restricted to only one routing rule we can add multiple rules as well. See below where we have added a new route .The Component is not restricted to only one routing rule we can add multiple rules as well.
@page "/"
@page "/index"
The NotFound issue
Start the application and try navigating to /something .You will see there is no component related to our route but the application still shows a component with the generic message ... Where does this message come from?
See App.razor
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
Let's replace the <NotFound> component (just the <p> section) with our own version called <CustomNotFound>
@page "/404"
@* a route that is not available, is by default a 404 page*@
@*We are using some Bootstrap classes and the
custom class customNotFound (in mystyle.css) which we create in the
css folder in wwwroot
Don't forget to modify the index.html file to point to the new
mystyle.css file*@
<div class="card customNotFound">
<div class="card-body">
<div class="row">
<div class="col">
We are sorry, but we couldn't find the page you are looking for!!!
</div>
</div>
</div>
</div>
mystyles.css
.customNotFound {
text-align: center;
color: red;
font-size: 35px;
box-shadow: 1px 1px 1px grey;
}
Finally we modify App.razor
@using Products.Pages;
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<CustomNotFound />
</LayoutView>
</NotFound>
</Router>
Products4
Here we modify the CustomNotFound page by adding a new button below the error message.
We are going to access the method NavigateToHome that will be located in a new partial class for this current component
<div class="row">
<div class="col text-md-center m-5 text-primary">
<button type="button" class="btn btn-primary" @onclick="NavigateToHome">Navigate to the Home Page</button>
</div>
</div>
public partial class CustomNotFound
{
//Here we use the [Inject] attribute to inject our
//service NavigationManager in the class and not the constructor
[Inject]
public NavigationManager NavigationManager { get; set; }
//Once we inject the service we can use the NavigateTo
//method to navigate to the provided URI
public void NavigateToHome()
{
NavigationManager.NavigateTo("/");
}
}
A URI is an identifier of a specific resource. Like a page, or book, or a document. A URL is special type of identifier that also tells you how to access it, such as HTTPs , FTP
Supplementary Demos
MoreRoutesParametersNav
In this example we are implementing Route Parameters two ways
via an anchor link
via a button which uses the NavigationManager Navigate command
@page "/authors"
@inject NavigationManager NavigationManager
<h3>Authors</h3>
<hr/>
<a href="authordetail/11">Rowlings with Author Id 11</a>
<br />
<a href="authordetail">Rowlings with no Author Id</a>
<br />
<a href="thedetails">Rowlings with no Author Id using route the details</a>
<button class="btn btn-primary" @onclick="Navigate">Rowlings Author Id 22</button>
@code {
private void Navigate()
{
NavigationManager.NavigateTo("authordetail/22");
}
}
AuthorDetail.razor
@page "/authordetail"
@page "/authordetail/{authorId:int}"
@page "/thedetails"
@*Lots of routing rules above
1) Basic
2) Route Parameters using an int not the default string
3) Totally different name ... still valid way to get to this page
*@
<h3>Author Detail</h3>
<hr/>
@if(authorId==0)
{
<h4>No Author ID entered</h4>
<p>Rowlings wrote Harry Potter </p>
}
else
{
<p> @authorId Rowlings wrote Harry Potter</p>
}
@code {
[Parameter]
public int authorId { get; set; }
}
AnotherRoutesParamNav (.NET 5)
Reviews basic routing and introduces working with Query String
<NavLink class="btn btn-secondary" href="advancedrouting?Param1=Blazor&Param2=Routing">
<span class="oi oi-list-rich" aria-hidden="true"></span> Go To ... Advanced Routing with Query String Parameters
</NavLink>
@page "/advancedrouting"
@inject NavigationManager NavigationManager
<h3>Advanced Routing</h3>
<h4>Parameter 1: @Param1</h4>
<h4>Parameter 2: @Param2</h4>
<p>
<button class="btn btn-primary" @onclick="LoadParameters">Load Query String Parameters</button>
<br />
<br />
<a href="/basicrouting">Return with Anchor Link</a>
<button class="btn btn-secondary" @onclick="BackWithNav">Return with Navigation</button>
</p>
@code {
//names of query string parameters coming in from BasicRouting.razor page
private string Param1;
private string Param2;
private void LoadParameters()
{
//This is how you parse the query string
var absoluteUri = new Uri(NavigationManager.Uri);
var queryParam = System.Web.HttpUtility.ParseQueryString(absoluteUri.Query);
Param1 = queryParam["Param1"];
Param2 = queryParam["Param2"];
//Note:
//URI is used to distinguish one resource from other regardless of the method used.
//URL provides the details about what type of protocol is to be used.
//URI doesn't contains the protocol specification. URL is a type of URI.
}
private void BackWithNav()
{
NavigationManager.NavigateTo("basicrouting");
}
}
AnotherRoutesParamQueryStringNET8
Reviews basic routing and introduces working with Query String in .NET 8 ... WebAssembly Standalone App (easier/shorter technique)
<NavLink class="btn btn-secondary" href="advancedrouting?newParam1=Blazor&newParam2=Routing">
<span class="oi oi-list-rich" aria-hidden="true"></span> Go To ... Advanced Routing with Query String Parameters (Newer Technique -> .NET 7/8)
</NavLink>
<div class="alert alert-primary">
<h4>Advanced Routing ... Newer Technique using SupplyParameterFromQuery</h4>
<h5>Parameter 1: @newParam1</h5>
<h5>Parameter 2: @newParam2</h5>
</div>
//New! to .NET7/8
//To indicate that the parameter can come from the query string
//we decorate the parameter with the SupplyParameterFromQuery attribute
[Parameter]
[SupplyParameterFromQuery]
public string? newParam1 { get; set; }
[Parameter]
[SupplyParameterFromQuery]
public string? newParam2 { get; set; }
Recap ... Five Ways to Pass Data Between Components in Blazor (check out link in Resources for deeper dive)
Route parameters
Note the implementation of the @page directive allows you to send multiple values at the same time.
@page "/updateemployee/{Id:int?}/{LName?}"
int employeeId = 12345;
string lastName = "Smith" ; navigationManager.NavigateTo($"/updateemployee/{employeeId}/{lastName}");
Querystring
Querystring parameters allow you to attach the values as querystrings in a URL – in a key=value format. This is more precise than using route parameters, in that you know which value goes with what parameter name
State containers
The problem with passing data via route parameters or querystring is that you can’t pass a complex object in the URL, because these methods are restricted to numeric and string types. And, generally speaking, it is not realistic to only pass either a string or integer. Sometimes you want to share complex objects with other components in the application. Here we use Dependency Injection
Component parameters
You use this method when you want to pass data to a nested component.
Cascading parameters
In the Lecture we will
Create a basic WebAssembly Calculator App which is able to do addition,subtraction,multiplication and division (BlazorCalcWebAssembly)
Incorporate Bootstrap classes to create the UI
Incorporate two way binding and the onclick event
Incorporate C# methods to handle each separate operation
<div class="col-sm-2">
<button @onclick="AddNumbers" class="btn btn-info">Add (+)</button>
</div>
Offer you the challenge to create a simple Blazor Application that can be used by an online store that only sells garden hoses of various styles and sizes or something related to one of your interests (HosesOrderApp)
Create a UI that lists the various garden hoses and beside each one a quantity desired
Incorporate a button to determine the total cost of an order including sales tax if applicable
Offer you several bonus challenges
Dealing Cards (DealingCardsExercise.pdf/DealingCards)
note the use of Bootstrap classes to create two column UI
<div class="row">
<div class="col-8">
</div>
<div class="col-4">
</div>
</div>
note the use of the range input (slider) with a min=1 and max =15
note the use of the CardPicker class
consisting of a public static string [] method PickSomeCards(int numberOfCards) that will return an array of pickedCards
the private static method RandomValue() which is used to randomly pick each card number
... and the private method RandomSuit() which is used to randomly pick the suit of the card (Spades,Hearts,Clubs,Diamonds)
Rolling Dice (DiceRollExercise)
Note the use of a string diceImage array which stores the src name of the 6 dice images that are stored in the images folder inside the wwwroot folder
Note the use of a simple Random Number Generator to pick the dice rolled.
Binary To Decimal Converter (BinaryToDecimalConverterExercise.pdf/BinaryDecimal/BinaryDecimalUpdated)
Note the implementation of the class BinaryDecimalConverter which has a public method ConvertToDecimal
public class BinaryDecimalConverter
{
public string Binary { get; set; }
public string Result { get; set; }
public void ConvertToDecimal()
{
try
{
//2 represents binary
Result = Convert.ToInt32(Binary, 2).ToString();
}
catch
{
Result = "Error: not a valid binary number ";
}
}
Updated version changes method to parameterized version
public string ConvertToDecimal(string Binary)
{
try
{
//2 represents binary
Result = Convert.ToInt32(Binary, 2).ToString();
}
catch
{
Result = "Error: not a valid binary number ";
}
return Result;
}
Supplementary Demo
BlazorImagesOnclick
Displays dynamic images based on image clicked
Implements C# methods with parameters to handle each separate operation
Since method called has a parameter we must use a lambda expression
@page "/displayingdynamicimages"
<h3>Displaying Dynamic Images</h3>
<p>Please select your preferred food:</p>
<div style="display: flex;">
<div @onclick=@(() => OnFoodClicked("pizza"))>
<img width="150" height="150" style="margin-right: 20px;" src="/images/pizza.jpg" />
</div>
<div @onclick=@(() => OnFoodClicked("burger"))>
<img width="150" height="150" style="margin-right: 20px;" src="/images/burger.jpg" />
</div>
<div @onclick=@(() => OnFoodClicked("salad"))>
<img width="150" height="150" style="margin-right: 20px;" src="/images/salad.jpg" />
</div>
</div>
@if (FavouriteFoodImageSource is not null)
{
<div style="margin-bottom:40px">
<p>You picked ...<em>@foodpicked.ToUpper()</em> ... as your Favourite Food </p>
@(foodpicked=="pizza" ? "Great you picked Pizza ":"")
<img src="@FavouriteFoodImageSource" width="300" height="300"/>
</div>
}
@code {
public string? FavouriteFoodImageSource { get; set; }
public string? foodpicked { get; set; }
public void OnFoodClicked(string food)
{
foodpicked = food;
FavouriteFoodImageSource = "images/" + food + ".jpg";
}
}
In this Lecture we will
Create a simple To Do List WebAssembly Application (BlazorToDoWebAssembly)
Implement a class to define the required To Do List information
public class TodoItem
{
public string Title { get; set; }
public bool IsDone { get; set; }
}
Declare a List to hold the class object
@code {
//Here we declare a list to hold our TodoItem objects named todo
private IList<TodoItem> todos = new List<TodoItem>();
private string newTodo;
//This method checks to make sure that text has been entered
//then adds a new item to the list and clears the input to allow
//adding another item
private void AddTodo()
{
if (!string.IsNullOrWhiteSpace(newTodo))
{
todos.Add(new TodoItem { Title = newTodo });
newTodo = "";
}
}
Alternate List methods
TodoItem thing = new TodoItem()
{
Title=newTodo,
IsDone=false
};
todos.Add(thing);
TodoItem x = new TodoItem();
x.Title = newTodo;
x.IsDone = false;
todos.Add(x);
Implement a UI which allows the user to add to the To Do and have it display the current state in an unordered list which incorporates a checkbox
<ul>
@foreach (var todo in todos)
{
<li>
<input type="checkbox" @bind="todo.IsDone" />
<input @bind="todo.Title" />
</li>
}
</ul>
<input placeholder="Thing to do" @bind="newTodo" />
<button @onclick="AddTodo">Add To Do</button>
Implement a header which shows the number of times "things" that are not yet complete.
<h3>To Do (@todos.Count(todo => !todo.IsDone))</h3>
Offer you the Challenge to update the To Do List app to indicate ,via color highlight, the status of a item on the list ... for instance red not completed, green completed.
ToDoStatusUpdate
@{
if (todo.IsDone)
{
<input class="badge-success" @bind="todo.Title" />
}
else
{
<input class="badge-danger" @bind="todo.Title" />
}
}
ToDoStatusUpdateConditional
@* Two Possible Versions ... nice use of conditional operator to determine class in one line *@
<input @bind="todo.Title" class="@(todo.IsDone ? "badge-success" : "badge-danger")" />
<input @bind="todo.Title" style="background-color:@(todo.IsDone ? "green" : "red")" />
Supplementary Demo
LoadingImagesServerApp
instead of a To Do List , we are displaying a list of images stores in the wwwroot folder (images folder)
@code {
string img = "/images/blazor.png";
string imgName = "Welcome To Blazor Image Loading";
List<string> fileList = new List<string>();
//C# lets you add the @ symbol in front of a string to create a
//verbatim string literal where backslashes are not interpreted as escape characters
string path = Directory.GetCurrentDirectory() + @"\wwwroot\images";
public void LoadImages()
{
//GetFiles ... returns an array of the full names (including paths) for the files in the specified directory
//[0] = "C:\\Temp\\Blazor\\LoadingImagesServerApp\\LoadingImagesServerApp\\wwwroot\\images\\bucks.png"
var files = Directory.GetFiles(path);
//GetFileName returns the file name and extension of the specified path string
//bucks.png
foreach (var file in files)
{
fileList.Add(Path.GetFileName(file));
}
}
public void ReadFile(string fileName)
{
imgName = fileName.Split(".")[0]; //this will extract the first part of the image name ... bucks ... not the png
img = "/images/" + fileName; //img = /images/bucks.png (notice direction of back slashes ... web format)
}
}
@page "/loadingpage"
@using System.IO
@*necessary declaration... allows us to use Directory and Path commands in the code section*@
<h3>Loading Images Demo </h3>
<div class="row">
<div class="col-md-3">
<button class="btn btn-primary" @onclick="LoadImages">Load Images</button>
<h4>List</h4>
@if (fileList != null && fileList.Count > 0)
{
int n = 0;
@foreach (string file in fileList)
{
n++;
<br />
<span>@n</span>
<span @onclick="@( () => ReadFile(file))" style="cursor:pointer; text-decoration:underline;color:blue">@file</span>
}
}
</div>
<div class="col-md-9">
<h5>@imgName </h5>
<div>
<img src="@img" style="width:400px;height:400px;" />
</div>
</div>
</div>
In this Lecture we will
Create a simple Weather Forecasting application which will serve to review many of the fundamentals we have covered including
Razor component construction
Directives
Parameters
Child content/templates
Routing
Event Handling
Data Binding
WeatherForecastPart1A
First we create a Razor Component Page called WeeklyForecast.razor
@page "/weeklyforecast"
<h3>Weekly Forecast</h3>
@*We are going to create a UI which initially displays a day
of weather forecast data.
We'll use hard coded values which will eventually be replaced
with data
Later we will take this prototype and decide where components
can be abstracted into reusuable bits.*@
@*Here we are using the Bootstrap card class to give a nice
appearance
Inside the card's body we are using a span element with the
Open Iconic classes io io-rain, this will render an icon
which represents the weather status (see wwwroot folder)
*@
<div class="card bg-light" style="width:18rem;">
<div class="card-body text-center">
<span class="h1 oi oi-rain"></span>
<h1 class="card-title">17 C°</h1>
<p class="card-text">
Rainy weather expected Monday
</p>
</div>
</div>
Note: .NET 8 update
Open Iconic no longer supported must use Bootstrap Icons
<span class="bi bi-cloud-rain-fill"></span>
Need to add link to index.html in wwwroot folder
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
WeatherForecastPart1B
Our page is taking shape, however we're building a weekly forecast and we currently have a single day. So we are going to modify the page so it can display five days of forecast data. Since we are repeating the cards we'll wrap the display in a flex-box container... from Bootstrap we use the class d-flex We repeat the display with a foreach loop ... relying on Razor
... Also with multiple cards being displayed we are removing the static width "width:18rem" and letting the element expand as needed. To create some whitespace m-2 or margin 2 CSS class is added
<div class="d-flex">
@foreach (var item in Enumerable.Range(1, 5))
{
<div class="card bg-light m-2">
<div class="card-body text-center">
<span class="h1 oi oi-rain"></span>
<h1 class="card-title">17 C°</h1>
<p class="card-text">
Rainy weather expected Monday
</p>
</div>
</div>
}
</div>
WeatherForecastPart1C
By now you can see a pattern emerge, where the individual day of weather is repeated. This repeated section could easily be a reusable component that encapsulates a day of weather... so we are going to abstract our component out of the WeeklyForecast page into it's own component file. We will put it into the Shared folder
<div class="d-flex">
@foreach (var item in Enumerable.Range(1, 5))
{
<WeatherDay></WeatherDay>
}
</div>
In the Shared folder we create a new Razor Component (non-routable) called WeatherDay.razor
<div class="card bg-light m-2">
<div class="card-body text-center">
<span class="h1 oi oi-rain"></span>
<h1 class="card-title">17 C°</h1>
<p class="card-text">
Rainy weather expected Monday
</p>
</div>
</div>
In this Lecture we will
Continue with our Weather Forecast Application development (WeatherForecastPart2)
In this next stage we implement Parameters within our WeatherDay component.
Now that our component's HTML is isolated inside of this component we can make this component more dynamic by allowing it to accept data using parameters.
So, for our weather component we need to show whether it's
Rainy, Sunny or Cloudy ... with an corresponding icon.
We also will display the temperature and day of the week.
Below we will set up ours parameters which will be used
by our data-binding to display their values more dynamically...
@code {
[Parameter]
public string Summary { get; set; }
[Parameter]
public int TemperatureC { get; set; }
[Parameter]
public DayOfWeek DayOfWeek { get; set; }
}
<div class="card bg-light m-2">
<div class="card-body text-center">
<span class="h1 oi oi-rain"></span>
<h1 class="card-title"> @TemperatureC C°</h1>
<p class="card-text">
@Summary weather expected @DayOfWeek
</p>
</div>
</div>
... Once our component has parameters that are data bound we can return to the WeeklyForecast page and update our component instance to make use of this new feature using a command like below
<WeatherDay TemperatureC="20" Summary="Cloudy" DayOfWeek="DayOfWeek.Friday">
</WeatherDay>
Here are the changes to the WeeklyForecast.razor page
@foreach (var item in Enumerable.Range(1, 5))
{
//Part2A
//our component now has parameters that are data bound so we can now
//update our component instance to make use of this new feature
<WeatherDay
TemperatureC="20"
Summary="Cloudy"
DayOfWeek="DayOfWeek.Friday" />
}
In this Lecture we will
Convert our Weather Forecast Application to the Server hosting model to implement a few new features (WeatherForecastServerPart1Updated)
Injecting a Service
OnInitializedAsync Lifecyle Method
Learn to work with a Service (really just a class ) that can be injected in to our WeeklyForecast.razor page
We learn leverage the already existing WeatherForecastService.cs in the Data folder of the Server Project ... we will modify it slightly
public class WeatherForecastService
{
//In Part 3 of our solution we are now going to Inject this WeatherForecastService
//on our WeatherDay page and use it to generate data.
//Here we can see there is a collection of Summaries that are used to fill
//the Summary property
private static readonly string[] Summaries = new[]
{
//"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
"Cloudy","Rainy","Sunny"
//List was simplified to correspond to the three icons available in our project
};
//This method generates 5 random WeatherForecast values based on a start date.
public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
{
var rng = new Random();
return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
}).ToArray());
}
Now we go back to our WeatherDay.razor component and focus in the Summary Parameter
//Here we determine which icon to show based on the Summary field in the
//WeatherForecastService (note the complex conditional statement ... ternary operators)
string IconCssClass =>
Summary == "Cloudy" ? "cloud" :
Summary == "Rainy" ? "rain" :
"sun";
Now in the HTML section we replace <span class="h1 oi oi-rain"></span> with <span class="h1 oi oi-@IconCssClass"></span>
Before we go back to our main page (WeeklyForecast.razor) and inject our service we must go to Program.cs (.NET 6 onward) and declare our service
builder.Services.AddSingleton<WeatherForecastService>();
Finally onto the WeeklyForecast.razor page
First our declarations at the top
@page "/weeklyforecast"
@using Data;
@inject WeatherForecastService WeatherService
Next we go down to the code section
//With the WeatherForecastService available we can now call the GetForecastAsync
//method which generates an array of WeatherForecast objects (class declared in the Data folder).
//Here we add a field to capture the data that will be displayed on the page
//
WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await WeatherService.GetForecastAsync(DateTime.Now);
}
... and finally back to our loop in the HTML section where we now loop through all our forecasts obtained in the OnInitializedAsync() Task above
@*Since we are no longer working with static data, we should consider the
possibility of null values The component will render before OnInitializedAsync executes and after therefore if the component renders while the forecast array is null a NullReferenceException will be thrown when entering the foreach loop To safeguard against this, we use an if/then statement to provide a
display when no data is present and prevent the exception*@
@if (forecasts == null)
{
<span>No Data</span>
}
else
{
@*foreach (var item in Enumerable.Range(1, 5))*@
@foreach (var forecast in forecasts)
{
//Part3
//Now that we are using data from our WeatherService we'll need to replace the
//static loop in our view with one that uses the forecasts array.
<WeatherDay TemperatureC="@forecast.TemperatureC"
Summary="@forecast.Summary"
DayOfWeek="@forecast.Date.DayOfWeek" />
}
}
Supplementary Demos
LifeCycleMethods
demos the use of OnParametersSetAsync()
protected async override Task OnParametersSetAsync()
All Razor Component have a well-defined Lifecycle, which is represented by synchronous and asynchronous lifecycle methods. You an override these methods to perform additional operations.
The list below highlights the Lifecycle methods of Razor Components.
OnInitialized() , OnInitializedAsync()
This method is invoked when a Razor Component is first initialized.
OnParametersSet(), OnParametersSetAsync()
This method is invoked when the values of the Component Parameters are applied.
ShouldRender()
This method is invoked before the Razor Component’s contents are rendered. If this method returns true then UI is refreshed else if it returns false then UI is not refreshed.
OnAfterRender(firstRender), OnAfterRenderAsync(firstRender)
This method is invoked after the component’s content is rendered. The bool parameter is true if this is the first time this method is invoked else false
GuessingNumberGame
Reviews and extend many concepts covered up and to this point of the course
uses a Partial class file
uses a local css file
uses a protected override void OnInitialized()
implements a class
implements Random numbers and a StringBuilder
In this Lecture we will
Complete our Weather Forecast Application (WeatherForecastServerPart2Updated)
Child Content Templates
RenderFragment
Event Handling
EventCallBack
Learn that to make our WeatherDay component more flexible we can add a template region. This area will allow use to insert HTML, Razor code or Components as child content.
To add a template to our WeatherDay component we'll make use of the
RenderFragment class. A RenderFragment represents a segment of UI content. The RenderFragment is added exactly like any other component parameter using a property and [Parameter] attribute. Recall Lecture 95.
In the WeatherDay.razor file in the code section we add
[Parameter]
public RenderFragment CustomMessage { get; set; }
In the WeatherDay.razor file HTML section we add
<span class="h1 oi oi-@IconCssClass"></span>
<h1 class="card-title"> @TemperatureC C°</h1>
@*This is where the template will be rendered*@
@CustomMessage
<p class="card-text">
@Summary weather expected @DayOfWeek
</p>
... and then we go back to the WeeklyForecast.razor page include a reference to this RenderFragment within the WeatherDay component reference.
<WeatherDay TemperatureC="@forecast.TemperatureC"
Summary="@forecast.Summary"
DayOfWeek="@forecast.Date.DayOfWeek" >
<CustomMessage>
@if (forecast.Summary=="Rainy")
{
<div class="alert alert-danger">
Tornado Warning!
</div>
}
</CustomMessage>
</WeatherDay>
Learn that thus far, the WeeklyForecast and WeatherDay components have most of the features we commonly find in component architecture. However, one important aspect of UI development hasn't been discussed yet, interactivity.
Now we will look at how to handle events by giving users the ability to select
an item shown in the weekly forecast. We are going to use the EventCallback delegate type . It is used to expose events across components. We are going to add the ability for a WeatherDay to be selected from the weekly forecast when the users click on a given day
First in the WeatherDay.razor page code section we add
[Parameter]
public EventCallback<DayOfWeek> OnSelected { get; set; }
this event will return a type of DayOfWeek ... we can use this value to identify with item triggered OnSelected
Next we code the Event Handler using InvokeAsync
void HandleOnSelected()
{
OnSelected.InvokeAsync(this.DayOfWeek);
}
Next we declare a new boolean Parameter called Selected and use it in a lambda expression to point to a bootstrap style which will be used to highlight the selected day.
[Parameter]
public bool Selected { get; set; }
private string SelectedCss => Selected ? "bg-primary text-white" : "bg-light";
The parameter is used to see if the component is in a selected state
We will use this property to bind the selected value to the component and the corresponding Bootstrap CSS class
If the item is selected the bg-primary text-white class is used otherwise the value will default to bg-light
While we are here we need to add a new property to the WeatherForecast .cs class
public bool Selected { get; set; }
Finally we can go up to the HTML in the WeatherDay.razor page and add our SelectedCss and our onclick event
<div class="card m-2 @SelectedCss" @onclick="HandleOnSelected">
<div class="card-body text-center">
<span class="h1 oi oi-@IconCssClass"></span>
<h1 class="card-title"> @TemperatureC C°</h1>
@*This is where the template will be rendered*@
@CustomMessage
<p class="card-text">
@Summary weather expected @DayOfWeek
</p>
</div>
</div>
Now we finally go back to our WeeklyForecast.razor page and reference the WeatherDay component with our new parameters
<WeatherDay TemperatureC="@forecast.TemperatureC"
Summary="@forecast.Summary"
DayOfWeek="@forecast.Date.DayOfWeek"
OnSelected="HandleItemSelected"
Selected="@forecast.Selected">
<CustomMessage>
@if (forecast.Summary=="Rainy")
{
<div class="alert alert-danger">
Tornado Warning!
</div>
}
</CustomMessage>
</WeatherDay>
Notice the reference to HandleItemSelected ... we need to add this method to complete our application
void HandleItemSelected(DayOfWeek selectedValue)
{
//clear selections
foreach (var item in forecasts)
item.Selected = false;
//Here we use a LINQ statement to find the matching DayOfWeek and toggle
//set the selected value to true
forecasts.First(f =>
f.Date.DayOfWeek == selectedValue).Selected = true;
}
Supplementary Demo
ToDoEventCallbackNET6
Blazor apps are the collection of multiple Blazor components interacting with each other and we are also allowed to use child components inside other parent components.
In real-world apps, it is a very common scenario to pass data or event information from one component to another component. Maybe you have a page in which user actions occurred in one component need to update some UI in other components. This type of communication is normally handled using an
EventCallback delegate.
In this demo, we illustrate how to use EventCallback to communicate between a parent and a child component.
First we declare a ToDo class in the Models folder
public class ToDo
{
public string? Title { get; set; }
public int? Minutes { get; set; }
}
Next we create our main non-routable component ToDoItem.razor which will use Parameters and an EventCallback
@using ToDoEventCallback.Models
@if (Item!=null)
{
<tr>
<td>@Item.Title</td>
<td>@Item.Minutes</td>
<td>
<button type="button" class="btn btn-success btn-sm float-right" @onclick="AddMinute">
+ Add Minutes
</button>
</td>
</tr>
}
@code {
[Parameter]
public ToDo? Item { get; set; }
[Parameter]
public EventCallback<MouseEventArgs> OnMinutesAdded{ get; set;}
public async Task AddMinute(MouseEventArgs e)
{
//Whenever a child component wants to communicate with the
//parent component,it invokes the parent component's callback
//method using InvokeAsync(Object)
if (Item!=null)
{
Item.Minutes += 1;
await OnMinutesAdded.InvokeAsync(e);
}
}
}
Initially you will notice the AddMinute method will add 1 to the Minutes property every time a user clicks the AddMinute button but the TotalMinutes property at the top of the page will stay the same. This is because the TotalMinutes property is calculated in the parent component and the parent component has no idea that the
Minutes are incrementing in the child component. So we need to facilitate the child to parent communication so that every time we add Minutes in the child component we will be able to update the parent UI... This is where the EventCallback comes in.
Now we can focus on our Routable page called ToDoList.razor where we will display our table and buttons to receive user input.
@page "/todolist"
@using ToDoEventCallback.Models
<div class="row">
<div class="col"><h3>To Do List</h3></div>
<div class="col"><h5 class="float-right">Total Minutes: @TotalMinutes</h5></div>
</div>
<br />
@if (ToDos==null)
{
<p><em>Loading ... </em></p>
}
else
{
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Title</th>
<th>Minutes</th>
<th></th>
</tr>
</thead>
<tbody>
@*Here we are passing each ToDo object inside the child
component (ToDoItem.razor ...located in shared folder)
using the Item property
*@
@foreach (var todo in ToDos)
{
<ToDoItem Item="todo" OnMinutesAdded="OnMinutesAddedHandler" />
}
</tbody>
</table>
}
@code {
public List<ToDo>? ToDos { get; set; } //stores list of ToDos
public int TotalMinutes { get; set; } //stores sum of all ToDo Item minutes
protected override void OnInitialized()
{
ToDos = new List<ToDo>()
{
new ToDo() {Title="Analysis", Minutes=40},
new ToDo() {Title="Design", Minutes=30},
new ToDo() {Title="Implementation", Minutes=75},
new ToDo() {Title="Testing", Minutes=40}
};
UpdateTotalMinutes();
}
public void UpdateTotalMinutes()
{
//Calculating the sum of Minutes property of all ToDo objects in the ToDos list
TotalMinutes = 0;
if (ToDos!=null)
{
foreach (var x in ToDos)
{
if(x.Minutes!=null)
{
TotalMinutes += (int) x.Minutes;
}
}
//TotalMinutes = ToDos.Sum(x => x.Minutes);
}
}
public void OnMinutesAddedHandler(MouseEventArgs e)
{
UpdateTotalMinutes();
}
}
In this Lecture we will
Begin the process of creating the classic TicTacToe game using Blazor WebAssembly (TicTacToeGame1.rar)
Set up the basic project structure
create our image folder and place all the images required for the app there (letter-o.png/letter-x.png)
create our main page Game.razor and place it in the Pages folder
link our Game.razor component via the NavMenu.razor in the shared folder
Begin to model the game
create a folder called Models
create an enumeration called PieceStyle.cs
//an enum (or enumeration type) is used to assign constant names
//to a group of numeric integer values.
//It makes constant values more readable,
//for example, WeekDays. Monday is more readable then number 0
//when referring to the day in a week.
public enum PieceStyle
{
X,
O,
Blank
}
create a class called GamePiece.cs
public class GamePiece
{
public PieceStyle Style;
//Default Constructor
public GamePiece()
{
Style = PieceStyle.Blank;
}
}
create the most important class called the GameBoard.cs and begin code some of the key elements
create an array of GamePieces
create the constructor which initializes/resets the game
create the Reset method which populates the board with blank pieces
public class GameBoard
{
//We are going to model the game pieces as a 2D array
public GamePiece[,] Board { get; set; }
//CurrentTurn tracks X then O then X then O etc
public PieceStyle CurrentTurn = PieceStyle.X;
//Default constructor
public GameBoard()
{
Reset(); //starts game with all blank squares
}
public void Reset()
{
Board = new GamePiece[3, 3];
//Fill board with blank pieces ... since GamePiece Constructor sets Style to PieceStyle.Blank
for (int r = 0; r < 3; r++)
{
for (int c = 0; c < 3; c++)
{
Board[r, c] = new GamePiece();
}
}
}
}
In this Lecture we will
Focus on getting the TicTacToe board to display onscreen (TicTacToeGame2)
Focus on the Game.razor page
add a connection to our Models folder
@using TicTacToeGame.Models
in @code section make an instance of the GameBoard
GameBoard board = new GameBoard
Now we code our required css which will be used to display our 3x3 board
we create style classes to ...
display the board
display the gamepiece (black border, green background)
incorporate a hover command
link x and o to images in the image folder and blank to a background-image:none
Lastly draw the board on screen
reference the classes in the css code
incorporate razor code which uses 2 loops (row and col) to draw the board
<h1>TicTacToe Game</h1>
@*display the tictactoe board
Note: the initial style of each location on the board is blank via GamePiece class
... so tictactoe-@board.Board[r,c].Style.ToString().ToLower .... results in
tictactoe-blank*@
<div class="tictactoe-board">
@for (int r = 0; r < 3; r++)
{
<div class="tictacttoe-column">
@for (int c = 0; c < 3; c++)
{
<div class="tictactoe-gamepiece
tictactoe-@board.Board[r,c].Style.ToString().ToLower()">
</div>
}
</div>
}
</div>
<hr />
@code {
GameBoard board = new GameBoard();
}
In this Lecture we will
Focus on the GameBoard.cs class (TicTacToeGame3)
introduce a property called CurrentTurn and have it use a method called SwitchTurns() which will keep track of who's turn it is .... X then O then X then O ... etc, etc
public PieceStyle CurrentTurn = PieceStyle.X;
private void SwitchTurns()
{
//If its currently X's turn make it O's turn next and vice versa
if (CurrentTurn == PieceStyle.X)
{
CurrentTurn = PieceStyle.O;
}
else
{
CurrentTurn = PieceStyle.X;
}
}
Introduce a property (GameComplete) to check if the game is complete
public bool GameComplete => GetWinner() != null || IsADraw();
we will create the IsADraw method in this lecture and the GetWinner method in the next lecture. A draw occurs when all the spaces are occupied and no winner can be found
public bool IsADraw()
{
int pieceBlankCount = 0;
bool status = false;
//A draw occurs when all spaces are occupied and no winner can be found
//Basically we are going to count all the spaces and if the
//count is 0 this a draw.
for (int r = 0; r < 3; r++)
{
for (int c = 0; c < 3; c++)
{
if (Board[r, c].Style == PieceStyle.Blank)
{
pieceBlankCount++;
}
}
}
if (pieceBlankCount == 0)
{
status = true;
}
else
{
status = false;
}
return status;
}
Game is complete when either a winner is declared or a draw occurs. These properties (variables) are used to monitor a players click.... They shouldn't be able to click any spaces if ..
the space is already occupied or the game is complete
GameComplete handles the second option .... then we use this property in the PieceClicked() method below
Create a method (PieceClicked) to allow players to claim spaces which also includes a check to make sure that they aren't already claimed and then ends with a call back to SwitchTurns.
public void PieceClicked(int x, int y)
{
//This method allows players to claim spaces which includes
//a check to make sure that they aren't already claimed
//we pass in the coordinates (x,y) / row,col of the space clicked
//If the game is complete, do nothing
if (GameComplete)
return;
//Check if space clicked on is empty
GamePiece clickedSpace = Board[x, y];
if (clickedSpace.Style == PieceStyle.Blank)
{
clickedSpace.Style = CurrentTurn;
SwitchTurns();
}
}
In this Lecture we will
Continue the coding of the GameBoard class and focus on determining who wins the game (TicTacToeGame4)
The method to check this is going to be brute-force. For each space on the board, check each possible direction for matching pieces, and if you find three in a row, stop checking
First implement an enumeration (EvaluationDirection) to show the directions from which we will look for tic-tac-toes
public enum EvaluationDirection
{
Up,
UpRight,
Right,
DownRight
}
Then we add a class called WinningPlay which declares three properties
a list of strings called WinningMoves
a WinningDirection of type EvaluationDirection
a WinningStyle property of type PieceStyle
public class WinningPlay
{
public List<string> WinningMoves { get; set; }
public EvaluationDirection WinningDirection { get; set; }
public PieceStyle WinningStyle { get; set; }
}
Create two methods within the GameBoard class that will work together to determine if we have a winner
EvaluatePieceForWinner(int i, int j) looks at a single given space and works Up,Right,UpRight and DownRight to see if we have 3 in a row. This method only evaluates for a single space ... we need the GetWinner() method to do that for all the spaces
private WinningPlay EvaluatePieceForWinner(int i, int j, EvaluationDirection dir)
{
GamePiece currentPiece = Board[i, j];
if (currentPiece.Style == PieceStyle.Blank)
{
return null;
}
int inARow = 1;
int iNext = i;
int jNext = j;
var winningMoves = new List<string>();
while (inARow < 3)
{
//We are using brute force
//For each direction we increment the iNext and or jNext
//to the next space to be evaluated
switch (dir)
{
case EvaluationDirection.Up:
iNext -= 1;
break;
case EvaluationDirection.UpRight:
jNext += 1;
iNext -= 1;
break;
case EvaluationDirection.Right:
jNext += 1;
break;
case EvaluationDirection.DownRight:
iNext += 1;
jNext += 1;
break;
}
//If the next space is off the board don't check it
if (iNext < 0 || iNext >= 3 || jNext < 0 || jNext >= 3)
break;
//If the next space has a matching letter
if (Board[iNext, jNext].Style == currentPiece.Style)
{
//add this space to the collection of winning spaces
//note: $ performs string concatention
//var name = "Sam";
//var msg = $"hello, {name}";
//Console.WriteLine(msg); // hello, Sam
winningMoves.Add($"{iNext},{jNext}");
inARow++;
}
else //no tic-tac-toe is found for this space direction
{
return null;
}
}
//If we found three in a row
if (inARow >= 3)
{
//Return this set of spaces as the winning set
winningMoves.Add($"{i},{j}");
return new WinningPlay()
{
WinningMoves = winningMoves,
WinningStyle = currentPiece.Style,
WinningDirection = dir,
};
}
//If we get this far and we didn't find any tic-tac-toes for the give space
return null;
//This method only evaluates for a single space... now we need a method
//to do that for all spaces ... so we need the method below GetWinner()
}
GetWinner() cycles through the entire 3x3 board and calls the method above ... EvaluatePieceForWinner
public WinningPlay GetWinner()
{
WinningPlay winningPlay = null;
for (int r = 0; r < 3; r++)
{
for (int c = 0; c < 3; c++)
{
//note: GetValues retrieves an array of values of the constants
//specified in the enumeration
foreach (EvaluationDirection evalDirection in (EvaluationDirection[])Enum.GetValues(typeof(EvaluationDirection)))
{
winningPlay = EvaluatePieceForWinner(r, c, evalDirection);
if (winningPlay != null)
return winningPlay;
}
}
}
return winningPlay;
}
Lastly, implement two more methods
GetGameCompleteMessage() gets a message to display the user X/O when the game is won or Draw
public string GetGameCompleteMessage()
{
var winningPlay = GetWinner();
if (winningPlay != null)
{
return $"{winningPlay.WinningStyle} Wins!";
}
else
{
return "Draw !";
}
}
IsGamePieceAWinningPiece(i,j) checks for winning situations ... This method kickstarts the search for a winner ... executed from the Game.razor page (next lecture)
public bool IsGamePieceAWinningPiece(int i, int j)
{
var winningPlay = GetWinner();
return winningPlay?.WinningMoves?.Contains($"{i},{j}") ?? false;
//?. is the null conditional operator
//The operator lets you access members and elements only when the receiver is not-null,
//returning null result otherwise.
//?? this the null coalescing operator
//... it returns the value of its left-hand operand if it isn't null
//otherwise it evaluates the right hand operand
//The null-coalescing operator was designed to be used easy with null-conditional operators.
//It provides default value when the outcome is null.
//int length = people?.Length ?? 0; // 0 if people is null
}
In this Lecture we will
Finally complete our game by adding a few necessary elements to our Game.razor page (TicTacToeGame5)
We add an onclick event (via Razor) to our board display
We add a style adjustment if we get 3 in a row (reduce opacity)
We add an if/else
If the game is NOT complete ... display who's turn it is
If game is complete ... make a call to the GetGameCompleteMessage (display winner or draw)
display a RESET button which when clicked clears the 3x3 board
@if (!board.GameComplete)
{
<h2>@board.CurrentTurn's Turn!</h2>
}
else
{
<h2>@board.GetGameCompleteMessage() <button class="btn btn-success" @onclick="board.Reset">Reset</button></h2>
}
@*display the tictactoe board
Note: the initial style of each location on the board is blank via GamePiece class
... so tictactoe-@board.Board[r,c].Style.TosString().ToLower .... results in
tictactoe-blank*@
<div class="tictactoe-board">
@for (int r = 0; r < 3; r++)
{
<div class="tictacttoe-column">
@for (int c = 0; c < 3; c++)
{
int x = r;
int y = c;
//the onclick is using a lambda expression since we are passing parameters ... can't use simple @onclick="MethodName"
//=> is lambda operator. When we don't have any input parameters we just use round brackets () before lambda operator.
//syntax: (input parameters) => expression
<div class="tictactoe-gamepiece tictactoe-@board.Board[r,c].Style.ToString().ToLower()"
@onclick="@(() => board.PieceClicked(x,y))" style="@(board.IsGamePieceAWinningPiece(r, c)? "opacity: 0.6" : "")">
</div>
//if you get 3 x's in a row or 3 o's in a row display with a reduced opacity of 0.6
}
</div>
}
</div>
Supplementary Demos and Exercises
TicTacToeGame5AlternateVersion
Slightly simplied version focusing in on the GameBoard.cs class
In the WinningPlay method ... simpler string concatentation technique
if (Board[iNext, jNext].Style == currentPiece.Style)
{
//add this space to the collection of winning spaces
//note: $ performs string concatention
//var name = "Sam";
//var msg = $"hello, {name}";
//Console.WriteLine(msg); // hello, Sam
//winningMoves.Add($"{iNext},{jNext}");
winningMoves.Add(iNext + "," + jNext);
inARow++;
}
In the IsGamePieceAWinningPiece method ... instead of the slick but complicated null conditional operator I use a simpler but longer set of if statements
var winningPlay = GetWinner();
if (winningPlay != null)
{
if (winningPlay.WinningMoves!=null)
{
return winningPlay.WinningMoves.Contains(i + "," + j);
}
return false;
}
//return winningPlay?.WinningMoves?.Contains($"{i},{j}") ?? false;
return false;
ConnectFourProblem
Uses many of the same techniques covered in the TicTacToe Game
Implements a Models folder ... with
PieceColor.cs class (enum with Red,Yellow,Blank)
GamePiece.cs class with constructor GamePiece(PieceColor color)
GameBoard.cs class which populates the 7x6 board with blank color
Most of the remaining code that was placed in GameBoard.cs in the TicTacToe game will go into the Game.razor page
EvaluationDirection.cs class (enum with Up,UpRight,Right,DownRight)
WinningPlay.cs class with public PieceColor WinningColor instead of PieceStyle
In the wwwroot folder the images folder has one small image of a pizza
The style classes implemented on the main page (ConnectFourPage.razor) are added to the bottom of the app.css file ... board,column,gamepiece,red,yellow
ConnectFourPage.razor ... this is where the majority of the logic is placed.
The UI section (HTML) is similiar to the TicTacToe game
The code section is where we have placed all the remaining code that used to be in the GameBoard.cs class
LetterMatchingGameProblem.pdf
LetterMatchingGame
Basic Letter Matching (click pairs of matching letters ... no images just basic text ) with a timer displaying total elapsed time until all matches clicked.
No classes or extra components all the code is in the Game.razor page.
Note the addition of @using System.Timers so that we can reference the Timer class
Note the use of an internal style sheet with the container class defining an area of 400 px and the redefining of a button with a width of 100px and height of 100px. This will give us 4 rows of 4 buttons across the screen
In the code section note the use of a the List of allLetters and the use of a Random Selection without duplication technique to randomly place the letters on the grid (Setup method)
In the ButtonClick method note the use of a Linq expression to replace correctly matched letters with blanks
//Replace found letters with empty string to
//hide them
randomLetters = randomLetters
.Select(a => a.Replace(letter, ""))
.ToList();
//... so let's say we clicked on A
//this would search and replace every occurence
//of A (there's two of them) with a blank
LetterMatchingGameUpdated
In this version we replace the simple letters with images
The images are stored in the wwwroot folder with names that match the letters from the previous version A...H and one extra the image of the letter X (we use this instead of a blank when we match two images)
This time in the ButtonClick method we replace a correctly chosen match with the image of X
randomLetters = randomLetters
.Select(a => a.Replace(letter, "X"))
.ToList();
In this Lecture we will
Start the process of creating a data driven Server Side Blazor app that keeps track of your favourite songs, the artist's name and the year of publication (BlazorSongLIst1/BlazorSongListNet6Part1)
Discuss how the application will talk to an SQL Server LocalDB using Entity Framework with the front-end capable of carrying out CRUD operations against the song data
Create a folder called Models and then create the Song class
public string SongId { get; set;}
public string Title { get; set; }
public string Artist { get; set; }
public string Year { get; set; }
Install the necessary packages to implement Entity Framework via Package Manager Console
Install-Package Microsoft.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.Design
Install-Package Microsoft.EntityFrameworkCore.Tools
Install-Package Microsoft.EntityFrameworkCore.SqlServer
If using .NET 6 or greater use the "Manage NuGet Packages for Solution instead of the Package Manager Console (in the Tools menu)
Add the connection string to the appsettings.json file
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=SongsDB;Trusted_Connection=True;MultipleActiveResultSets=true"
},
Create a context class in the Data folder called SongDbContext
The Context class is the most important class when working with EF Core. The Context class is used to query or save data to the database and it can also be used to perform the CRUD operations (Create, Read, Update, Delete) ... we will do the CRUD operations in a specially created class called a Service in the next Lecture
public DbSet<Song> Songs { get; set; }
public SongDbContext(DbContextOptions<SongDbContext> options) : base(options) { }
//protected methods are just like private methods, they can only be
//accessed by the members of the class only difference is they can be
//accessed by the derived classes as well
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Song>().HasData(
new
{
SongId = Guid.NewGuid().ToString(),
Title = "Thriller",
Artist = "Michael Jackson",
Year = "1980"
}, new
{
SongId = Guid.NewGuid().ToString(),
Title = "Shower",
Artist = "James Taylor",
Year = "1973"
}
);
}
Go into the Startup.cs file and link the connection string to the context class in the ConfigureServices method
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
services.AddDbContext<SongDbContext>(
option => option.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
}
Using .NET 6 or greater no Startup.cs must use Program.cs
builder.Services.AddDbContext<SongDbContext>(
option => option.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
Create the physical database (now that our Entity Framework Core is all set up) by performing the migration and update of the database via Package Manager Console
add-migration Initial
update-database
view contents of database via SQL Server Object Explorer
In this Lecture we will
Create a service (class) in the Data folder which will contain methods to perform the CRUD operations .... Recall in Razor pages (Lecture 76-77) how Scaffolding created all these methods for us ... NO Scaffolding in Blazor Server
We will build this custom service and use it with a component (UI page Songs.razor) by injecting it via Dependency Injection ... recall Lecture 94 ... State Management (BlazorSongList2/BlazorSongListNet6Part2)
create a new class called SongService.cs
public class SongService
{
SongDbContext _context;
public SongService(SongDbContext context)
{
_context = context;
}
public async Task<List<Song>> GetSongsAsync()
{
var result = _context.Songs;
return await Task.FromResult(result.ToList());
}
public async Task<Song> GetSongByIdAsync(string id)
{
return await _context.Songs.FindAsync(id);
}
public async Task<Song> InsertSongAsync(Song song)
{
_context.Songs.Add(song);
await _context.SaveChangesAsync();
return song;
}
public async Task<Song> UpdateSongAsync(string id, Song s)
{
var song = await _context.Songs.FindAsync(id);
if (song == null)
return null;
song.Title = s.Title;
song.Artist = s.Artist;
song.Year = s.Year;
_context.Songs.Update(song);
await _context.SaveChangesAsync();
return song;
}
public async Task<Song> DeleteSongAsync(string id)
{
var song = await _context.Songs.FindAsync(id);
if (song == null)
return null;
_context.Songs.Remove(song);
await _context.SaveChangesAsync();
return song;
}
private bool SongExists(string id)
{
return _context.Songs.Any(e => e.SongId == id);
}
}
Register this service in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
services.AddScoped<SongService>();
services.AddDbContext<SongDbContext>(
option => option.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
}
.NET 6 or higher you register the service in Program.cs
builder.Services.AddScoped<SongService>();
Start creating our User Interface page (Razor Component) using FetchData.razor as a guide (copy and paste code into new component)
Create the Razor component Songs.razor... paste in FetchData.razor contents and then modify .
@page "/songs"
@using BlazorSongLIst.Data
@using BlazorSongLIst.Models
@inject SongService songService
<h1>My Song List</h1>
<p>This component demonstrates managing a Song list.</p>
@if (songs == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Artist</th>
<th>Date</th>
</tr>
</thead>
<tbody>
@foreach (var item in songs)
{
<tr>
<td>@item.SongId</td>
<td>@item.Title</td>
<td>@item.Artist</td>
<td>@item.Year</td>
</tr>
}
</tbody>
</table>
}
@code {
List<Song> songs;
protected override async Task OnInitializedAsync()
{
await load();//uses method below
}
protected async Task load()
{
songs = await songService.GetSongsAsync();
}
}
Add a link in the NavMenu.razor page to Songs.razor and test out the first version of the UI
In this Lecture we will
Focus on implementing all the CRUD operations in the Songs.razor page ... so far we only have (R)ead functional
First we add some instance variables for the 4 columns (fields) of our database in the code section of the Songs.razor page and an enum to represent the CRUD choices.
string songId;
string title;
string artist;
string year;
private enum MODE { None, Add, EditDelete };
MODE mode = MODE.None;
Next we insert a button just above the data display to ADD a new record in the HTML section
<button @onclick="@Add" class="btn btn-success">Add</button>
When the ADD button is clicked we want a simple insert form to appear below the data (Update and Delete use a different form)
@if (songs != null && mode==MODE.Add ) // Insert form
{
<input placeholder="Title" @bind="@title" />
<br />
<input placeholder="Artist" @bind="@artist" />
<br />
<input placeholder="Year" @bind="@year" />
<br />
<button @onclick="@Insert" class="btn btn-warning">Insert</button>
}
Now we need to create the Add and Insert methods and associated helpers in the code section
protected void Add()
{
ClearFields();
mode = MODE.Add;
}
protected void ClearFields()
{
songId = string.Empty;
title = string.Empty;
artist = string.Empty;
year = string.Empty;
}
protected async Task Insert()
{
Song s = new Song()
{
SongId = Guid.NewGuid().ToString(),
Title = title,
Artist = artist,
Year = year
};
await songService.InsertSongAsync(s);
ClearFields();
await load();
mode = MODE.None;
}
In this Lecture we will
Complete the Song List Database ... focusing on UI Songs.razor page (BlazorSongLIst4)
Add the necessary code for Updating (Editing) and Deleting records
First we add an onclick event to each record displayed so that when a record is clicked it will display a new form which can be used to either Edit / Delete the clicked on record
<tbody>
@foreach (var item in songs )
{
<tr @onclick="@(() => Show(item.SongId))">
<td>@item.SongId</td>
<td>@item.Title</td>
<td>@item.Artist</td>
<td>@item.Year</td>
</tr>
}
</tbody>
Then before coding the Show method we make an instance of the Song class
Song song
Next we code the Show method ... accepting the SongId as the key identifier
The Show method is called by an onclick event in the HTML code above while displaying the contents of each record. In essence when you click on a specific row it will determine the id of the row you clicked on and use
that information to determine all the column contents. It then sets the mode to EditDelete ... which is used by the 3rd If code block to display
the Update (Edit) and Delete Form
protected async Task Show(string id)
{
song = await songService.GetSongByIdAsync(id);
songId = song.SongId;
title = song.Title;
artist = song.Artist;
year = song.Year;
mode = MODE.EditDelete;
}
Now we write some Razor code in the HTML section to check for Mode.EditDelete so we can popup a new form
@if (songs != null && mode == MODE.EditDelete) // Update & Delete form
{
<input type="hidden" @bind="@songId" />
<br />
<input placeholder="First Name" @bind="@title" />
<br />
<input placeholder="Last Name" @bind="@artist" />
<br />
<input placeholder="School" @bind="@year" />
<br />
<button @onclick="@Update" class="btn btn-primary">Update</button>
<span> </span>
<button @onclick="@Delete" class="btn btn-danger">Delete</button>
}
To finish off the app we just need to code the Update(Edit) and Delete methods
protected async Task Update()
{
Song s = new Song()
{
SongId = songId,
Title = title,
Artist = artist,
Year = year
};
await songService.UpdateSongAsync(songId, s);
ClearFields();
await load();
mode = MODE.None;
}
protected async Task Delete()
{
await songService.DeleteSongAsync(songId);
ClearFields();
await load();
mode = MODE.None;
}
... and finally remove songId from UI display
Supplementary Demo
BlazorSongLIst4UpdateEmptyAddEditCheck
Adds extra layer of error checking making sure empty records are not added or updated to the Database
protected async Task Insert()
{
Song s = new Song()
{
SongId = Guid.NewGuid().ToString(),
Title = title,
Artist = artist,
Year = year
};
if (!string.IsNullOrEmpty(s.Title) && !string.IsNullOrEmpty(s.Artist))
{
await songService.InsertSongAsync(s);
ClearFields();
await load();
mode = MODE.None;
}
}
protected async Task Update()
{
//Here we make an instance of the Song class
//and store our local instance variables obtained by the
//form into class variables
Song s = new Song()
{
SongId = songId,
Title = title,
Artist = artist,
Year = year
};
if (!string.IsNullOrEmpty(s.Title) && !string.IsNullOrEmpty(s.Artist))
{
await songService.UpdateSongAsync(songId, s);
ClearFields();
await load();
mode = MODE.None;
}
Leave you with the challenge to create your own Blazor Database Application to track information related to a topic of your interest
In this Lecture we will
Discuss the concept of Open Data
Open data is data that anyone can access, use and share. Governments, businesses and individuals can use and share open data to bring about social, economic and environmental benefits.
See "What Is Open Data?" in the Resources
Discuss how most of these Open Data sites provide an interface for programmers to make specific calls for data through an Application Programming Interface (API).
APIs are sets of requirements that govern how one application can communicate and interact with another. They allow your application to interact with an external service using a simple set of commands.
See "What is an API (and what do they have to do with restaurants)? " in the Resources for a deeper explanation
We will be accessing data services that return data in JSON format (they implement the JSON:API)
Learn that JSON stands for JavaScript Object Notation.
JSON objects are used for transferring data between server and client
JSON is a text based data exchange format that uses key:value pairs.
JSON style:
{"students":[
{"name":"John", "age":"23", "city":"Agra"},
{"name":"Steve", "age":"28", "city":"Delhi"},
{"name":"Peter", "age":"32", "city":"Chennai"},
{"name":"Chaitanya", "age":"28", "city":"Bangalore"}
]}
XML style:
<students>
<student>
<name>John</name> <age>23</age> <city>Agra</city>
</student>
<student>
<name>Steve</name> <age>28</age> <city>Delhi</city>
</student>
<student>
<name>Peter</name> <age>32</age> <city>Chennai</city>
</student>
<student>
<name>Chaitanya</name> <age>28</age> <city>Bangalore</city>
</student>
</students>
JSON is much more light-weight compared to XML, easier to read and write, text based , human readable data exchange format
In the above example we have stored information about a number of students in an Array of Objects called students that has 3 properties
To access the information out of this array we could say
students[0].age ... output would be 23
students[2].name ... output would be Peter
Supplementary Resources
30+ Amazing Free Data Sources You Need to Use in 2024
A collective list of free APIs
In this Lecture we will
Create a simple Blazor Server Application which will access the NHTSA site and extract Honda models information in JSON format. Sometimes we refer to this process as "Consuming a Web API"
https://vpic.nhtsa.dot.gov/api/vehicles/getmodelsformake/honda?format=json
Take a deep look at the JSON file
{"Count":614,"Message":"Response returned successfully","SearchCriteria":"Make:honda","Results":[{"Make_ID":474,"Make_Name":"HONDA","Model_ID":1861,"Model_Name":"Accord"},{"Make_ID":474,"Make_Name":"HONDA","Model_ID":1863,"Model_Name":"Civic"},{"Make_ID":474,"Make_Name":"HONDA","Model_ID":1864,"Model_Name":"Pilot"},{"Make_ID":474,"Make_Name":"HONDA","Model_ID":1865,"Model_Name":"CR-V"},{"Make_ID":474,"Make_Name":"HONDA","Model_ID":1866,"Model_Name":"Ridgeline"},{"Make_ID":474,"Make_Name":"HONDA","Model_ID":1868,"Model_Name":"Element"},{"Make_ID":474,"Make_Name":"HONDA","Model_ID":1869,"Model_Name":"Odyssey"},{"Make_ID":474,"Make_Name":"HONDA","Model_ID":1870,"Model_Name":"Insight"},
Create a Car class that contains fields/properties that mirror the JSON file (CarsAPI)
public class Car
{
public int Make_ID { get; set; }
public string Make_Name { get; set; }
public int Model_ID { get; set; }
public string Model_Name { get; set; }
}
Here we define the necessary fields/properties that match those of the JSON file
A little review of some Class concepts
A Property encapsulates a private field. It provides getters(get{ }) to retrieve the value of the underlying field and setters(set{ }) to set the value of the underlying field. In the example below, _myPropertyVar is a private field that cannot be accessed directly. It will only be accessed via MyProperty. So MyProperty encapsulates myPropertyVar
private int myPropertyVar;
public int MyProperty
{
get { return _myPropertyVar; }
set { _myPropertyVar = value; }
}
Auto-implemented Property
From C# 3.0 onwards, property declaration has been made easy if you don't want to apply some logic in get or set.
The following is an example of an auto-implemented property:
public int MyAutoImplementedProperty { get; set; }
Notice that there is no private backing field in the above property example. The backing field will be created automatically by the compiler. You can work with an automated property as you would with a normal property of the class. Automated-implemented property is just for easy declaration of the property when no additional logic is required in the property accessors.
Create a new class called Data which contains an array of objects of type Car called Results
Where the instance name matches the JSON top level element name Results which holds the list of Honda models
public Car[] Results { get; set; }
Create the UI page (razor component) called CarsClient.razor which will be used to display the JSON file contents in a table format
@page "/carsclient"
@using Models
@using System.Text.Json @*Needed for JsonSerializer*@
@inject IHttpClientFactory HttpClientFactory
@*Must add services.AddHttpClient to Startup.cs or Program.cs*@
<h4>Showing All Honda Car Models</h4>
<table class="table m-4 country-table">
<thead>
<tr>
<th>Make ID</th>
<th>Make Name</th>
<th>Model ID</th>
<th>Model Name</th>
</tr>
</thead>
<tbody>
@foreach (var car in Cars)
{
<tr>
<td>@car.Make_ID</td>
<td>@car.Make_Name</td>
<td>@car.Model_ID</td>
<td>@car.Model_Name</td>
</tr>
}
</tbody>
</table>
@code {
Car[] Cars { get; set; }
protected override async Task OnInitializedAsync()
{
//List of all Honda Models
var url = "https://vpic.nhtsa.dot.gov/api/vehicles/getmodelsformake/honda?format=json";
var client = HttpClientFactory.CreateClient();
var response = await client.GetAsync(url);
var responseStream = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<Data>(responseStream);
Cars = data.Results;
}
Note the use of a class called country-table ... this was added to site.css in the wwwroot folder
.country-table {
width: 90%;
margin: 1.5rem auto;
}
.country-table th {
width: 25%;
cursor: pointer;
}
Offer you a Challenge to modify the CarsAPI application to include a search box where a user can enter the car manufacturer of their choice and after pressing a button display all the car models from the requested manufacturer.
SearchableCarDatabaseProblem.pdf
CarsAPISearch
CarsAPISearchUpdated
Offer you a Second Challenge to access the JSON file associated with the url : https://outlier.oliversturm.com/countries and display the contents in a table format in a Blazor Server Application (WorldDemo)
{"data":[{"_id":"58060596392c9a92f2f86222","name":"American Samoa","areaKM2":199,"population":57880},{"_id":"58060596392c9a92f2f86223","name":"Afghanistan","areaKM2":647500,"population":29929000},{"_id":"58060596392c9a92f2f86224","name":"Andorra","areaKM2":468,"population":70550},{"_id":"58060596392c9a92f2f86225","name":"Angola","areaKM2":1246700,"population":11190800},{"_id":"58060596392c9a92f2f86226","name":"Anguilla","areaKM2":102,"population":13250},{"_id":"58060596392c9a92f2f86227","name":"Antigua & Barbuda","areaKM2":443,"population":68720},{"_id":"58060596392c9a92f2f86228","name":"Argentina","areaKM2":2766890,"population":39537900},
Offer you a Third Challenge to find a public url (containing data in JSON format) of your choice and display the data in a Blazor app
Supplementary Demos
PostalCodeAPIproblem.pdf
... Check out the Resource Link: Zippopotamus - Zip Codes Galore
Structure of api is : api.zippopotum.us/country/postal-code
Example: https://api.zippopotam.us/us/90210
This returns the JSON file
{
"post code": "90210",
"country": "United States",
"country abbreviation": "US",
"places": [
{
"place name": "Beverly Hills",
"longitude": "-118.4065",
"state": "California",
"state abbreviation": "CA",
"latitude": "34.0901"
}
]
}
Notice that some of the names of the fields have spaces ... you will need to use the command [JsonPropertyName("post code")] when defining the properties in your class(es)
Note the field "places" defines an array with fields ... place name,longitude etc
ZipCodesAPIappWASM
Here we are using a WebAssembly App so we will implement the HttpClient class instead of the IHttpclientFactory class. This is actually a lot less complicated than the Server App version
We use @inject HttpClient Http ... should be automatically declared in Program.cs
Our app requires a postal code and the country code (entered via a dropdown list that displays the full name of all the countries but binds to the required Country Code). We obtain these country codes via another api located here : https://date.nager.at/api/v3/AvailableCountries
In the Models folder we define Country and Zip info.
public class Country
{
//This class will be referenced when we consume the
//api located at https://date.nager.at/api/v3/AvailableCountries
//This api returns most of the country codes and names in the world
public string countryCode { get; set; }
public string name { get; set; }
}
public class ZipPrimaryInfo
{
//in the json format response from the api
//the parameter name is actually post code
//... but we can't use blanks when defining our properties names
//... so JsonPropertyName tells Blazor the property name that is
//present when serializing and deserializing
[JsonPropertyName("post code")]
public string post_code { get; set; }
public string country { get; set; }
[JsonPropertyName("country abbreviation")]
public string country_abbreviation { get; set; }
//places ... is the exact name in the json output from the api
//it is used as an array/list
//{"post code": "90210", "country": "United States", "country abbreviation": "US",
//"places": [{"place name": "Beverly Hills", "longitude": "-118.4065", "state": "California",
//"state abbreviation": "CA", "latitude": "34.0901"}]}
public List<ZipPlaceInfo> places { get; set; }
}
public class ZipPlaceInfo
{
//We declared a separate class for the places array fields
//{"post code": "90210", "country": "United States", "country abbreviation": "US",
//"places": [{"place name": "Beverly Hills", "longitude": "-118.4065", "state": "California",
//"state abbreviation": "CA", "latitude": "34.0901"}]}
[JsonPropertyName("place name")]
public string place_name { get; set; }
public string longitude { get; set; }
public string latitude { get; set; }
public string state { get; set; }
[JsonPropertyName("state abbreviaton")]
public string state_abbreviation { get; set; }
}
Note the ease of loading in the Json API results
protected override async Task OnInitializedAsync()
{
var url = "https://date.nager.at/api/v3/AvailableCountries"; //this saves us having to enter by hand every country code in the world
countries = await Http.GetFromJsonAsync<List<Country>>(url); //loaded automatically when app executes
}
PublicHolidayAPIwasmUpdated
... Check out the Resource Link World Wide Public Holidays
The structure of the api is : api/v3/PublicHolidays/{Year}/{CountryCode}
A typical call to the api looks like : https://date.nager.at/api/v3/publicholidays/2024/AT
A typical JSON file response looks like
{
"date": "2017-01-01",
"localName": "Neujahr",
"name": "New Year's Day",
"countryCode": "AT",
"fixed": true,
"global": true,
"counties": null,
"launchYear": 1967,
"types": [
"Public"
]
Key Steps
1) This is a WebAssembly App
We implement the HttpClient class instead of the IHttpClientFactory class.
This is actually a lot less complicated than the Server App version.
@inject HttpClient Http
2) We create Models folder and create 3 classes
HolidayRequestModel
HolidayResponseModel
Country (to be used by the second API call)
In the Models folder we declare a HolidayRequrestModel and HolidayResponseModel
public class HolidayRequestModel
{
//This class will be referenced as the Model in the EditForm of the HolidayExplorer Page
//These are the two required fields/properties when accessing the Holiday API
[Required]
public string CountryCode { get; set; }
[Required]
[Range(1900, 2100, ErrorMessage = "Choose years between 1900 and 2100")]
public int Year { get; set; }
}
public class HolidayResponseModel
{
//This class mirrors most of the values the Holiday API returns
public string Name { get; set; }
public string LocalName { get; set; }
public DateTime? Date { get; set; }
public string CountryCode { get; set; }
public bool Global { get; set; }
public string PublicHolidayType { get; set; }
}
The code section
@code {
private HolidayRequestModel HolidaysModel = new HolidayRequestModel();
private List<HolidayResponseModel> Holidays = new List<HolidayResponseModel>();
//countries field is an instance of the Country class
//that will store the entire list of possible countries available from the API.
//This will be used by the InputSelect component in the EditForm
public List<Country> countries = new List<Country>();
//This method is called from the EditForm's OnValidSubmit event
private async Task HandleValidSubmit()
{
//HolidaysModel contains HolidaysModel.CountryCode and HolidaysModel.Year which will be passed to the API
//The values returned from the API will be stored in the Holidays object list and instance of the HolidayResponseModel
var url = "https://date.nager.at/api/v3/PublicHolidays/" + HolidaysModel.Year +"/"+ HolidaysModel.CountryCode;
Holidays = await Http.GetFromJsonAsync<List<HolidayResponseModel>>(url);
}
protected override async Task OnInitializedAsync()
{
//This will get all the available countries (countryCode,name) via the api which will make things alot more efficient.
//Saves us having to type them all in by hand. We will use the results in the InputSelect component
//https://date.nager.at/swagger/index.html
var url = "https://date.nager.at/api/v3/AvailableCountries";
countries = await Http.GetFromJsonAsync<List<Country>>(url);
}
private void HandleReset()
{
HolidaysModel = new();
Holidays = new();
}
}
In this Lecture we will
Create a Blazor WebAssembly application that implements a movie filter which takes a hardcoded list of classic movies and makes it possible to filter that list by entering a search term.
Begin with the UI first and rough out a simple search field that will temporarily display the value the user enters via @SearchTerm just to make sure everything is working properly (MovieSearchPart1)
@page "/moviesearchclient"
@inherits IndexBase
<div class="container">
<div class="form-group">
<input class="form-control"
@bind-value="SearchTerm" id="search"
placeholder="Enter movie to search for ..." />
</div>
</div>
@SearchTerm
I have also told this component to inherit from IndexBase ... similiar to the code behind file from Webforms (we are not going to use @code in this application)
Create a new class called MovieSearchClient.razor.cs (same name as the UI above ) in the Pages folder
This class is called a Base Class
Component base classes are simply C# classes from which your components can inherit.
Once you have one, you can put as much or as little of your UI code as you want in there and your component will still be able to make use of it.
You create this base class by naming it the exact same name as the corresponding
razor page and you add .cs to it. It is essentially like code behind from web forms.
namespace MovieSearch.Pages
{
public class IndexBase:ComponentBase
{
protected string SearchTerm { get; set; }
}
}
Test out the current code to make sure the entered SearchTerm is recognized after it is entered in the search field.
In this Lecture we will
Focus on actually showing some search results (MovieSearchPart2)
Create a Data folder in the root of the project and add a new class called Movie
public class Movie
{
public string Title { get; set; }
public string Year { get; set; }
public string Image { get; set; }
}
Create a list of movies which we can filter based on the search term the user will enter (Title).
public class IndexBase:ComponentBase
{
protected string SearchTerm { get; set; }
List<Data.Movie> Movies { get; set; } = new List<Data.Movie>
{
new Data.Movie {Title="12 Angry Men",Year="1957",Image="12AngryMen1957.jpg"},
new Data.Movie {Title="2001 A Space Odyssey",Year="1968",Image="2001ASpaceOdyssey1968.jpg"},
new Data.Movie {Title="Casablanca",Year="1942",Image="Casablanca1942.jpg"},
new Data.Movie {Title="China Town",Year="1974",Image="ChinaTown1974.jpg"},
new Data.Movie {Title="Citizen Kane",Year="1941",Image="CitizenKane1941.jpg"},
new Data.Movie {Title="ET",Year="1982",Image="ET1982.jpg"},
new Data.Movie {Title="Forrest Gump",Year="1994",Image="ForrestGump1994.jpg"},
new Data.Movie {Title="Gone With The Wind",Year="1939",Image="GoneWithTheWind1939.jpg"},
new Data.Movie {Title="Lawrence Of Arabia",Year="1962",Image="LawrenceOfArabia1962.jpg"},
new Data.Movie {Title="One Flew Over The Cuckoos Nest",Year="1975",Image="OneFlewOverTheCuckoosNest1975.jpg"},
new Data.Movie {Title="On The Waterfront",Year="1954",Image="OnTheWaterfront1954.jpg"},
new Data.Movie {Title="Psycho",Year="1960",Image="Psycho1960.jpg"},
new Data.Movie {Title="Raging Bull",Year="1980",Image="RagingBull1980.jpg"},
new Data.Movie {Title="Schindlers List",Year="1993",Image="SchindlersList1993.jpg"},
new Data.Movie {Title="Star Wars Episode IV",Year="1977",Image="StarWarsEpisodeIV1977.jpg"},
new Data.Movie {Title="Sunset Blvd",Year="1950",Image="SunsetBlvd1950.jpg"},
new Data.Movie {Title="The Bridge On The River Kwai",Year="1957",Image="TheBridgeOnTheRiverKwai1957.jpg"},
new Data.Movie {Title="The GodFather",Year="1972",Image="TheGodFather1972.jpg"},
new Data.Movie {Title="The GodFather Part 2",Year="1974",Image="TheGodFatherPart21974.jpg"},
new Data.Movie {Title="The Shawshank Redemption",Year="1994",Image="TheShawshankRedemption1994.jpg"},
new Data.Movie {Title="The Silence Of Lambs",Year="1991",Image="TheSilenceOfTheLambs1991.jpg"},
new Data.Movie {Title="The Sound Of Music",Year="1965",Image="TheSoundOfMusic1965.jpg"},
new Data.Movie {Title="The Wizard Of Oz",Year="1939",Image="TheWizardOfOz1939.jpg"},
new Data.Movie {Title="Vertigo",Year="1958",Image="Vertigo1958.jpg"},
new Data.Movie {Title="West Side Story",Year="1961",Image="WestSideStory1961.jpg"},
};
}
Create a folder called "images" in the wwwroot folder and copy all the relevant images there
Create a read only property which returns a filtered list by adding the code below to the IndexBase class
protected IEnumerable<Data.Movie> SearchResults =>
Movies.Where(p =>
string.IsNullOrEmpty(SearchTerm)
|| p.Title.ToLower().Contains(SearchTerm.ToLower())
);
Note use of IEnumerable
This allows readonly access to a collection
Then a collection that implements IEnumerable can be used with a for-each statement.
SearchResults will either:
return the full list if SearchTerm is null or empty
... or return a filtered list where the movie title contains the search term (ignoring case)
This means we'll always get the complete list of movies if a search term has not been entered and a filtered list of movies if one has.
Head back to our main UI page and add markup to loop over the search results.
<div class="container">
<h1>Greatest Movies Of All Time</h1>
<div class="form-group">
<input class="form-control"
@bind-value="SearchTerm" id="search"
placeholder="Enter movie to search for ..."
@bind-value:event="oninput" />
</div>
<div class="row align-items-center border-bottom">
<div class="col-sm">
<h5 class="mt-0">Movie Poster Image</h5>
</div>
<div class="col-sm">
<h5 class="mt-0">Title</h5>
</div>
<div class="col-sm">
<h5 class="mt-0">Year of Release</h5>
</div>
</div>
@foreach (var item in SearchResults)
{
<div class="row align-items-center border-bottom">
<div class="col-sm">
<img src="images/@item.Image" class="mr-3" />
</div>
<div class="col-sm">
<h5 class="mt-0">@item.Title</h5>
</div>
<div class="col-sm">
<h5 class="mt-0">@item.Year</h5>
</div>
</div>
}
</div>
The @foreach loops through every item in a List called SearchResults and renders details for each one ... with a little help from Bootstrap
Also notice the addtion of @bind-value:event which allows us to tell Blazor precisely when we want it to update SearchTerm. The default is onchange which is triggered when the input loses focus: alternately, oninput happens every single time you press a key while the input has focus. This will allow us to have results update instantaneously as you type terms in the filter/search
Offer you the Challenge to modify the application to also accept "YEAR" as a search parameter
MovieSearchPart2Year
protected IEnumerable<Data.Movie> SearchResults =>
Movies.Where(p =>
string.IsNullOrEmpty(SearchTerm)
|| p.Title.ToLower().Contains(SearchTerm.ToLower())
|| p.Year.Contains(SearchTerm)
);
MovieSearchPart2YearUpdatedPartialClass
implements a Partial class
adds ... using MovieSearch.Data
reduces line ... List<Data.Movie> Movies { get; set; } = new List<Data.Movie>
to ... List<Movie> Movies { get; set; } = new List<Movie>
The UI uses a classic table format instead of
<div class="row">
<div class="col">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Movie Poster Image</th>
<th>Title</th>
<th>Year</th>
</tr>
</thead>
<tbody>
@foreach (var item in SearchResults)
{
<tr>
<td><img src="images/@item.Image" height="100" width="90" /></td>
<td>@item.Title</td>
<td>@item.Year</td>
</tr>
}
</tbody>
</table>
Offer you a Second Challenge to search through a hard-coded "Todo" list
ToDoSearchProblem.pdf
ToDoSearch
@page "/todoclient"
@using Data
<input @bind-value="@SearchTerm"
@bind-value:event="oninput"/>
@*ml-5 margin left*@
<span class="text-muted ml-5">
Showing @FilteredToDos.Count out of @ToDoItems.Count
</span>
<h4 class="mt-4">To Do's</h4>
<ul>
@foreach (var toDo in FilteredToDos)
{
<li>@toDo.Name</li>
}
</ul>
@code {
//Initialize SearchTerm to "" to prevent nulls
string SearchTerm { get; set; } = "";
//Simple hard coded data
List<ToDoItem> ToDoItems => new List<ToDoItem>
{
new ToDoItem {Name="Groceries"},
new ToDoItem{Name="Laundry"},
new ToDoItem{Name="Garbage"},
new ToDoItem{Name="Cut grass"},
new ToDoItem{Name="Dentist"},
new ToDoItem{Name="Car Maintenance"},
new ToDoItem{Name="Clean carpets"}
};
//returns full list initially because SearchTerm=""
List<ToDoItem> FilteredToDos => ToDoItems.Where(i => i.Name.ToLower().Contains(SearchTerm.ToLower())).ToList();
}
Supplementary Demo
SelectDemo
A simple list of People's Names and Cities is displayed
A Dropdown list allows the user to choose a specific city
The application highlights the table row of each person who lives in the specified city
All kinds of neat ideas here
First the code section
@code {
public List<People> peopleList = new List<People>
{
new People{Name="Tom Burrows",City="New York"},
new People{Name="Bruce Wayne",City="Gotham"},
new People{Name="Clark Kent",City="Metropolis"},
new People{Name="Mary Jones",City="Toronto"},
new People{Name="Frances Lord",City="New York"}
};
//Instead of typing out all the possible cities we leverage the peopleList
//and pull out every distinct city and use it to form our Cities list
public IEnumerable<string> Cities => peopleList.Select(c => c.City).Distinct();
public string? SelectedCity { get; set; }
//This is where we are Filtering based on City and highlighting the table row
public string GetCss(string city) => SelectedCity == city ? "bg-info text-white" : "";
}
Here is the HTML section
<table class="table table-sm table-bordered table-striped table-hover">
<thead>
<tr>
<th>Name</th>
<th>City</th>
</tr>
</thead>
<tbody>
@foreach (var p in peopleList)
{
<tr class="@GetCss(p.City)">
<td>@p.Name</td>
<td>@p.City</td>
</tr>
}
</tbody>
</table>
<div class="form-group">
<label for="city">City</label>
<select name="city" class="form-select" @bind="SelectedCity">
<option disabled selected>Select City to Search and Highlight</option>
@foreach(var city in Cities)
{
<option value="@city" >
@city
</option>
}
</select>
</div>
In this Lecture we will
Pose the question: Blazor is here, is JavaScript dead? The answer is NO at-least for now.
Even though Blazor is creating a revolution in web technology, it still needs JavaScript to use web features that cannot be achieved by Blazor for now.
Discuss how there are a number of features WebAssembly does not support and therefore Blazor does not have direct access to them. These are typically features such as
Accessing the Browser's DOM ... like accessing a Button element
Media Capture
Popups
Web Storage
Learn that to access these browser features we need to use JavaScript as an intermediary between Blazor and the Browser. To do this Blazor has provided JavaScript Interop support that will enable us to do use JavaScript functions from C# and vice versa.
Learn how to call Javascript functions with C# methods in a Blazor WebAssembly Application.
With the help of JSInterop (JavaScript Interoperability) the user can invoke Javascript functions
To call the JavaScript method from .NET, the user can use the IJSRuntime abstraction. This abstraction offers the InvokeAsync<T> method
This method accepts the function name (identifier) and any number of arguments that the function requires as an argument.
A simple call to use the InvokeAsync<T> method would look something like this.
var result = await JSRuntime.InvokeAsync<string>("function name", input);
Create a simple application which calls a JavaScript function
BlazorJSInterop1
First we need to add the JavaScript file into the wwwroot folder...create a subfolder called js and add the JavaScript file called myscript.js
function getDateTime() {
return new Date();
}
Second step is to add the script tag to the index.html file
Note: Using .NET8 Blazor WebApp ... Interactive WebAssembly ... there is no index.html file
You must add the reference to the javascript file in the App.razor file
See ... BlazorJSInterop1NET8webappWASM
Third step is to create a component page called TheDatePage.razor
Inject the JSRuntime dependency
@inject IJSRuntime JSRuntime
Code some HTML to create a simple UI having a button that when clicked will call Invoke that accesses the getDateTime function and updates the UI
<div>
<div>
<p>Current DateTime: <b>@CurrentDate</b></p>
<br/>
<input type="button" @onclick="GetCurrentDateTime" value="Get Current DateTime"/>
</div>
</div>
The final step is to add the C# code
@code {
public string CurrentDate;
private async void GetCurrentDateTime()
{
CurrentDate = await JSRuntime.InvokeAsync<string>("getDateTime");
StateHasChanged();
}
}
// Must call StateHasChanged() because Blazor
// will not know to refresh page because
// it was updated by JavaScript
In this Lecture we will
Create two more simple applications which cover different situations and different ways to interact with Javascript code from our Blazor application.
BlazorJSInterop2
Call JavaScript Functions from C# when JS Functions Return Void
function displayAlert() {
alert("Welcome to my Javascript Alert called from Blazor");
}
//note the use of InvokeVoidAsync ....calling a method that does not return a value
public async Task ShowAlert()
{
await JsRuntime.InvokeVoidAsync(identifier: "displayAlert");
}
Using C# to Call JavaScript Functions that Return a Value and accesses an HTML element via the DOM (Document Object Model)
<h4>Creating an Prompt Here ... passing a parameter ... returns something</h4>
<input @bind="questionText" />
<button class="btn btn-warning" @onclick="AskQuestion">Ask Question</button>
@*html above displays the javascript prompt and accepts our answer but does nothing with it
so below we are providing an html element that can be used to display the answer returned
.... next we need to create a simple Javascript function that takes our inputted text
and places it into the element below
.... this function will be called at the same time in the AskQuestion method in the code section below*@
<div>
The answer was : <span id="answerSpan"></span>
</div>
public async Task AskQuestion()
{
var response = await JsRuntime.InvokeAsync<string>(identifier: "displayPrompt", questionText);
//this code will take the answer from the prompt (response) and display it on the browser in the specified DOM element
await JsRuntime.InvokeVoidAsync("setElementTextById", "answerSpan", response);
}
function displayPrompt(question) {
return prompt(question);
}
function setElementTextById(id, text) {
document.getElementById(id).innerText = text;
}
BlazorJSInterop3
Reviews calling a JS function that returns void (simple alert... this time it also accepts a passed parameter) and a JS function that returns a result (prompt)
Implement a partial class which will be used to invoke calls to our JavaScript functions
public partial class JSinBlazorPage
{
[Inject]
public IJSRuntime JSRuntime { get; set; }
private string registrationResult;
public async Task ShowAlertWindow()
{
await JSRuntime.InvokeVoidAsync(identifier:"displayAlert", "Javascript function called from Blazor");
}
public async Task RegisterEmail()
{
registrationResult = await JSRuntime.InvokeAsync<string>("emailReg", "Please enter your email");
}
}
function displayAlert(message) {
alert(message);
}
function emailReg(message) {
const result = prompt(message);
if (result == null)
return 'Please provide an email';
const returnMessage = 'Hi ' + result.split('@')[0] + ' your email ' + result + ' has been accepted';
return returnMessage;
}
Offer you the challenge to create a simple Blazor Server App that changes the Text on a Button
BlazorButtonJS
The focus in this example is having your Blazor app access a DOM element which cannot be done in Blazor proper but rath requires Javascript to do
@page "/main"
@inject IJSRuntime jsRuntime
<h1> Accessing the DOM</h1>
<button id="btn" @onclick="UpdateTitle">Update Title</button>
@code {
private async void UpdateTitle()
{
await jsRuntime.InvokeVoidAsync("accessDOMElement","btn","Title Changed");
}
}
function accessDOMElement(id,text) {
// access DOM here
document.getElementById(id).innerText = text;
}
In this Lecture we will
Look at a practical application of calling JavaScript in an Blazor Application
For now, there is no built-in functionality for saving a file in Blazor.
It is necessary to write the function in Javascript as you would with more traditional web applications and then invoke the JavaScript function from within our Blazor application's C# code.
BlazorSaveAFileUpdated
function SaveTextFile(filename, fileContent) {
var link = document.createElement('a');
link.download = filename;
link.href = "data:text/plain; charset=utf-8, " + encodeURIComponent(fileContent)
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
@page "/thesavepage"
@inject IJSRuntime JSRuntime
<h1>Saving A Text File in Blazor using JSInterop </h1>
<textarea @bind="noteContent" />
<br />
<button @onclick="SaveFile">Save Text</button>
@code {
string noteContent;
string fileName = "note.txt";
public async Task SaveFile()
{
await JSRuntime.InvokeVoidAsync("SaveTextFile",fileName,noteContent);
}
}
Offer you the challenge to create a Blazor WebAssembly Application that will append a row to an HTML Table .
AddRowProblem.pdf
AddRowJS
@page "/tablechange"
@inject IJSRuntime JSRuntime
<h4 class="bg-info text-white">Adding a new row to a table via JS</h4>
<div class="form-group">
<table class="table table-sm table-bordered table-striped ">
<thead>
<tr>
<th>Music Artist</th>
<th>Country</th>
</tr>
</thead>
<tbody @ref="RowReference">
<tr>
<td>Drake</td>
<td>Canada</td>
</tr>
<tr>
<td>Beyonce</td>
<td>USA</td>
</tr>
</tbody>
</table>
<button class="m-2 btn btn-secondary" @onclick="AddRow">Add a new Row</button>
</div>
@code {
//In this code I have used @ref attribute on the tbody element
//so that I can create a reference to it.
//Inside the button’s click, I am calling the JS function called “AddTableRow”
//and passing this reference of the tbody element as first argument.
//I have also passed the artist name and the country
//as the second and third arguments to this function.
public ElementReference RowReference { get; set; }
public async Task AddRow()
{
await JSRuntime.InvokeVoidAsync("AddTableRow", RowReference, "The Rolling Stones", "Great Britain");
}
}
function AddTableRow(elem, artist, country) {
/*The row variable contains a tr element containing 2 td's
* that contain the artist name and country sent from Blazor*/
let row = document.createElement("tr");
let cell1 = document.createElement("td");
let cell2 = document.createElement("td");
cell1.innerText = artist;
cell2.innerText = country;
row.append(cell1);
row.append(cell2);
elem.append(row);
}
/*This function receives the reference of the tbody element as the first argument
and in the last line of the code appends the row element to it
*/
Supplementary Demos
BlazorAudio
Simple application used to demonstrate how to implement mp3 sounds via Javascript and the HTML audio tag (Media Player)
Method 1 (Playing one sound which is hard coded via the src to the Media Player)
@page "/audiopageone"
@inject IJSRuntime JsRuntime
<h3>Blazor Audio Demo Page One</h3>
<p>To play the sound, click the button: <button id="soundButton" @onclick="PlaySound">Click Here</button></p>
<p>To make the sound stop playing, click this button: <button id="stopButton" @onclick="PauseSound">Stop sound!</button></p>
@*This element tells the browser to render a simple media player to the HTML page ... with the controls visible*@
<audio controls id="simpsons" src="../sounds/912.mp3" />
@code {
//We are passing the ID of the audio element on the page
public async Task PlaySound()
{
await JsRuntime.InvokeVoidAsync("PlayAudio", "simpsons");
}
public async Task PauseSound()
{
await JsRuntime.InvokeVoidAsync("PauseAudio", "simpsons");
}
}
function PlayAudio(elementName) {
document.getElementById(elementName).play();
}
function PauseAudio(elementName) {
document.getElementById(elementName).pause();
}
Method 2 (Playing multiple sounds via the Media Player based on a reference to the source id ... no harded coded sound)
@page "/audiopagetwo"
@inject IJSRuntime JsRuntime
<h3>Blazor Audio Demo Page Two</h3>
<button class="btn btn-primary" @onclick="PlayAudioFile1">Okly</button>
<span> </span>
<button class="btn btn-primary" @onclick="PlayAudioFile2">Toot</button>
<audio id="player">
<source id="playerSource" src="" />
@code {
//Here we are passing the actual audio src instead of the id
//as we did in the first demo
async Task PlayAudioFile1()
{
await JsRuntime.InvokeVoidAsync("PlayAudioFile", "/sounds/okly.mp3");
}
async Task PlayAudioFile2()
{
await JsRuntime.InvokeVoidAsync("PlayAudioFile", "/sounds/toot.mp3");
}
}
//This code finds the audio player, sets the source file
//loads it and plays it
function PlayAudioFile(src) {
var audio = document.getElementById('player');
if (audio != null)
{
var audiosource = document.getElementById('playerSource');
if (audiosource != null)
{
audiosource.src = src;
audio.load();
audio.play();
}
}
}
BlazorScrollUpJS
This example demonstrates how you can call a Javascript method using JS Interop to scroll to the top of a page.
In this app a button event triggers when the button is clicked (at the bottom of the page) and the associated Javascript code forces the page to scroll to the top .
Scroll down and click the button; it scrolls to the top position.
CookieConsent
This application demostrates how to implement a Cookie Consent Banner in Blazor WebAssembly Standalone App .NET 9.
Since WASM apps run client side the HttpContext features are not available to check for cookies. This app uses JavaScript Interop to set and get cookies and does not rely on any third party library (NUGet Package) like Blazored LocalStorage to access local storage.
This application uses some simple Javascript code added to the index.html page in the www.root folder to set and get the cookie (localStorage.setItem and localStorage.getItem)
It uses a Razor Component (ConsentCookie.razor) to display the cookie consent banner
The ConsentCookie.razor component is added to the MainLayout
The cookie consent banner is displayed only if the cookie has not been set. The user can accept the cookie by clicking the Accept Cookies button.
BlazorQRserverNet9
This is a simple .NET 9 Blazor WebApp Server Render Mode application that generates and reads QR codes.
The QR Read component uses some help from Javascript
The QR Create component uses QRCoder Library (NuGet Package). Written in C#.NET, it enables you to create QR codes. It hasn't any dependencies to external libraries
Program Details QR Create
Most of the key coding happens in QrCodeServices.cs file located in the Services folder. We declare it as a Singleton in Program.cs
The rest of the coding happens in the QRcreate.razor page where we inject the service and call it qrCodeImageUrl = QrCodeService.GenerateQrCodeImageUrl(inputText);
Make special note of the ImageSave Task which gives you the ability to save the QR image (named by you) to an image folder in the wwwroot. The key here is the use of IWebHostEnvironment env (Dependency Injection) and then using the env.WebRootPath to get the path to the wwwroot folder.
Program Details QR Read
The Key code in this application happens in the js file called QRscript.js located in the js folder of the wwwrootfolder.
We also need to make sure we point to this js file and the actual library by updating App.razor with the required script src values
It uses the jsQR library to decode the QR code image. The QRread.razor page injects the IJSRuntime and calls the JS function decodeQrCode(imageDataUrl) to get the QR content.
BlazorBarCodesUpdated
This application provides a modern barcode reading experience using Blazor WebAssembly Standalone App .NET 9.
Features:
BarcodePage – Scan barcodes or QR codes using your webcam or decode from uploaded images. Supports multiple formats including PDF417 and Data Matrix.
Toggle options for continuous decoding, and all formats for flexible scanning.
Real-time results and error feedback are displayed for a smooth user experience.
Powered by ZXingBlazor NuGet package for fast and reliable barcode recognition.
How to use:
Go to the BarcodePage to start scanning.
Choose to scan via webcam or upload an image.
Adjust decoding options to suit your needs.
Tip:Try different barcode formats and see instant results. The app is designed for both quick scans and batch processing.
BlazorSpeechToTextDemoJS
This demo showcases real-time speech-to-text capabilities in a Blazor WebAssembly Standalone app.
There are many options for speech-to-text:
Cloud APIs (Azure Cognitive Services, Google, etc.). Native libraries wrapped with gRPC or REST. Browser Web Speech API.
For a Blazor app that runs in the browser, the Web Speech API is the easiest place to start:
No server round trips for audio. No external billing. Works directly in the user’s browser. The downside: not all browsers support it (mainly Chromium based ones do: Chrome, Edge, some versions of Opera). You will add a simple IsSupported check and show a friendly message when support is missing.
The solution is built using:
A small JavaScript helper speech-to-text.js that wraps the Web Speech API for browser-based voice input.
Real-time transcription with interim and final results.
Error handling and browser support checks.
A reusable Blazor component (SpeechToText) to drive speech recognition and integrate with your forms.
A simple binding pattern (bind-Value) to plug speech-to-text into any form field.
User Benefits:
This feature makes your app feel more modern and friendly, especially on mobile devices or for long forms. Users can dictate feedback or other text, making data entry faster and more accessible.
Technical Highlights:
Works entirely in the browser—no server round-trips for speech recognition.
Easy to reuse: drop the SpeechToText component into any form.
Real-time interim and final transcript display.
Status line: listening / idle / error message.
Clear integration pattern for parent-child communication (e.g., clearing form fields).
All the magic will happen in JavaScript, but Blazor will control it via JS interop, and you will get typed callbacks on the .NET side.
Try it out: Enter your name and feedback. You can type or use your voice. Click Clear All to reset the form and start again!
BlazorSpeechToTextEmailWebAppServerJS
This Updated demo showcases real-time speech-to-text capabilities in a Blazor Web App Interactive Render Mode Server with the additional capability of sending the transcribed text to other users via Email
For speech-to-text we are using:
Browser Web Speech API.
This updated solution implements:
The Email Sending Library called MailKit
MailKit: A popular .NET library for sending and receiving emails using standard protocols like SMTP, IMAP, and POP3
Technical Highlights:
Sending email in a Blazor Server application is generally easier than in a Blazor WebAssembly application due to the fundamental differences in their execution environments.
In Blazor Server, your application code runs entirely on the server. This means you have direct access to server-side resources and libraries, including those for sending emails.
Direct SMTP Client Access: You can directly use the SmtpClient class from System.Net.Mail or any other .NET email library (e.g., MailKit) to connect to an SMTP server and send emails.
To test out our implementation we used the Ethereal Site
Ethereal is a fake SMTP service, mostly aimed at Nodemailer and EmailEngine users (but not limited to). It's a completely free anti-transactional email service where messages never get delivered. Instead, you can generate a vanity email account right from Nodemailer, send an email using that account just as you would with any other SMTP provider and finally preview the sent message here as no emails are actually delivered.
Try it out: Enter your Name, Receipent Email, Subject Heading and Body Text (Speech to Text Transcribed). You can also type your body content. Click Clear All to reset the form and start again!
In this Lecture we will
Learn how to host a Blazor WebAssembly application (Client side Blazor app) as a static website in Azure Storage
Recall that Blazor WebAssembly is a Single Page App (SPA) framework for building client side web apps with .NET.
SPAs are static sites which means we can use Azure Storage to serve our app
We don't need an APP Service, just a simple storage account in Azure, so that we can copy our files there using the Static Web Option
This is the concept of Serverless Web App Architecture
Instead of hitting some server we hit some files in storage
It will return back the index.html and some code that needs to be rendered in the browser.
That's where WebAssembly (underling universal technology that let's us run compiled code) and Blazor comes in.
Learn how to host the WebAssembly app called BlazorCalcWebAssembly (created in the first lecture of this section) in Azure.
Sign up for a free Azure account ... then
Go to Azure portal
Click the Create a Resource button
Select Storage account ... This will open the Create Storage Account
Select an Azure Subscription (free trial)
Select Resource Group or Create New (BlazorDemo1)
Fill in a name for the Storage account (chiarelliblazor)
Choose a Location
Pick Standard Performance
Make sure that the account is a StorageV2 account
Click Review + create
Click Create
When the Storage account is created, we can enable it's Static Website feature
Click on the static website menu-item
Click enable
Fill in the index.html as the index document name. This will be the default file that is used when we navigate to the website
Click Save to enable the setting
This Azure Storage Account can now host a static website. Note the Primary endpoint URL . This is the URL that we use to reach the website
The key idea we are using here is that our Blazor WebAssembly App is not ASP.NET Core hosted. We are going to publish in Azure Storage because it's cheaper.
Drill down into Blob Storage (Blob service ... Containers)
The static website feature looks for the files in the $web container that it created
Click on the $web container (that's where all the required files for your app will go)
To be able to host the Blazor app we need to publish the files that we need to run it.
In Visual Studio open the app called BlazorCalcWebAssembly
Right click the Blazor project name and click Publish
Choose Folder as the publish target and click Publish to publish the files
Navigate to the published files (these are the files we will need to copy to Azure)
bin->Release->netstandard2.1->publish->wwwroot
_framework
css
sample-data
index.html
There is only one more thing to do: copy the files we just published into the Azure Storage account.
We will do that using the Azure Storage Explorer app which is a free tool that you can use to manage Azure Storage.
Open the Azure Storage Explorer and make sure that you are signed into your Azure account
Drill down into Storage Account -> Blob Containers -> $web
Now open an explorer window to the folder where the Blazor app was published
Select the contents of this folder and drag them onto the storage explorer window. It will automatically upload the files into the $web container
Now go to the browser and paste in the Primary endpoint URL of the Azure storage static website and view your Blazor app in the clouds!
Learn that we have just deployed the app manually, but you also can go straight to Azure from within Visual Studio
In this Lecture we will
Discuss the importance of Form Validations and re-visit how they have been implemented throughout the course
Webforms
Lecture11ValidatedCustomerForm
MVC
Lecture58RaptorsModelValidation
Razor Pages
Lecture82RaptorsCoreRazor
Look back at some Blazor apps which have forms with no validation as a preamble to implementing form validations in Blazor
Lecture93BirthdayProblem
Lecture97BlazorCalcWebAssembly
Lecture111BlazorSongList4
We can use the standard HTML form and input elements to create a Blazor form. However Blazor comes with several built-in forms and input components that make the task of creating a form much easier. These built-in Blazor form components also support form validation. We will focus on this validation idea in the upcoming videos
In this Lecture we will
Learn that the EditForm component is Blazor's approach to managing user input in a way that makes it easy to perform validations against user input. It provides the ability to check if all validations rules have been satisfied and present the user with validation errors if they have not.
Learn that although it is possible to create forms using the standard <form> HTML element we will use the EditForm component because of the additional features it provides.
Learn that the key feature to the EditForm is its Model parameter. This parameter provides the component with a context it can work with to enable user-interface binding and determine whether or not the user's input is valid
Begin to create a simple Blazor WebAssembly application to illustrate some of these ideas
FormValidationIntro1
We begin by creating a simple empty class called Student
public class Student
{
}
We create a new page (FormValidationPage.razor) that will reference this class to perform a basic page submission
@page "/formvalidationpage"
@using FormValidationIntro.Models
<h3>Form Validation Page</h3>
<h5>Status: @Status</h5>
<EditForm Model="@Std" OnSubmit="@FormSubmitted">
<input type="submit" value="Submit" class="btn btn-primary"/>
</EditForm>
@*Instead of input you can also use button
<button type="submit" class="btn btn-primary">Submit</button>
*@
FormValidationIntro2
Because the EditForm component basically renders a standard <form> HTML element it is actually possible to use standard HTML form elements such as <input> and <select> but we will focus on using Blazor input controls because they come with additional functionality such as validation
Here are some standard input components available in Blazor
InputText/InputTextArea (single line and multi line)
InputNumber (numeric up down control)
InputDate (pops up a Datepicker)
InputSelect (dropdown list)
InputCheckbox (binds a Boolean property)
As we work our way to a fully validated form the next step is to flesh out our empty Student class
public string Name { get; set; }
public int Age { get; set; }
public DateTime Birthdate { get; set; }
public string BackgroundInfo { get; set; }
public bool Cookies { get; set; }
Next we update the form with a few of the Blazor input components
<EditForm Model="@Std" OnSubmit="@FormSubmitted">
<input type="submit" value="Submit" class="btn btn-primary" />
<br />
<br />
<InputText @bind-Value=Std.Name />
<br />
<br />
<InputNumber @bind-Value=Std.Age />
<br />
<br />
<InputDate @bind-Value=Std.Birthdate />
<br />
<br />
<InputTextArea @bind-Value=Std.BackgroundInfo />
<br />
<br />
<InputCheckbox @bind-Value=Std.Cookies />
</EditForm>
In this Lecture we will
Complete our simple application by implementing the actual validation
The DataAnnotationsValidator is the standard validator type in Blazor. By simply adding this component within the EditForm it will enable validation for the entire form.
FormValidationIntro3
First we need to go back into our Student class and decorate its properties with some data annotations for validation. The data annotations will control the validation. The form validation controls that we add later will simply trigger and display the results of these validation rules.
public class Student
{
[Required]
public string Name { get; set; }
[Required]
[Range(18,65,ErrorMessage ="Age must be between 18 and 65")]
public int Age { get; set; }
public DateTime Birthdate { get; set; }
[StringLength(400,ErrorMessage ="400 characters max")]
public string BackgroundInfo { get; set; }
public bool Cookies { get; set; }
}
Next we will edit/enhance our FormValidationPage.razor to accept information about a student.
Note that the label tag allows you to click on the label, and it will be treated like clicking on the associated input element. There are two ways to create this associations.
One way is to wrap the label element around the input element.
<label>Input here:
<input type='text' name='theinput' id='theinput'>
</label>
The second way is to use the for attribute, giving it the ID of the associated input.
<label for="theinput">Input here:</label>
<input type='text' name='whatever' id='theinput'>
This is especially useful for use with checkboxes and buttons,
since it means you can check the box by clicking on the associated
text instead of having to hit the box itself.
<EditForm Model="@Std" OnSubmit="@FormSubmitted">
<div class="form-group">
<label for="Name">Name:</label>
<InputText @bind-Value=Std.Name class="form-control" id="Name" />
</div>
<div class="form-group">
<label for="Age">Age:</label>
<InputNumber @bind-Value=Std.Age class="form-control" id="Age" />
</div>
<div class="form-group">
<label for="Birthdate">Birthdate:</label>
<InputDate @bind-Value=Std.Birthdate class="form-control" id="Birthdate" />
</div>
<div class="form-group">
<label for="BackgroundInfo">Background Info:</label>
<InputTextArea @bind-Value=Std.BackgroundInfo class="form-control" id="BackgroundInfo" />
</div>
<div class="form-group">
<label for="Cookies">Accept Cookies:</label>
<InputCheckbox @bind-Value=Std.Cookies class="form-control" id="Cookies" />
</div>
<input type="submit" class="btn btn-primary" value="Save"/>
</EditForm>
Running the app now will result in the user being presented with a form that does not validate their input. To ensure the form is validated we must specify a validation mechanism. We do this by adding a DataAnnotationsValidator component inside the EditForm
<EditForm Model="@Std">
<DataAnnotationsValidator />
<div class="form-group">
<label for="Name">Name:</label>
<InputText @bind-Value=Std.Name class="form-control" id="Name" />
</div>
Running the app now and clicking the Save button will update the user interface and give us a small visual indication that there are errors but without any specifics
Next we need to a way to display either a comprehensive list of all the errors in the form or a way to display error messages for a specific input on the form. We do this using ValidationSummary and/or ValidationMessage
The ValidationSummary component can simply be dropped into our EditForm with no additional parameters
<EditForm Model="@Std">
<DataAnnotationsValidator />
<ValidationSummary/>
The ValidationMessage component displays error messages for a single field
<EditForm Model="@Std">
<DataAnnotationsValidator />
<ValidationSummary/>
<div class="form-group">
<label for="Name">Name:</label>
<InputText @bind-Value=Std.Name class="form-control" id="Name" />
<ValidationMessage For=@(() => Std.Name)/>
</div>
<div class="form-group">
<label for="Age">Age:</label>
<InputNumber @bind-Value=Std.Age class="form-control" id="Age" />
<ValidationMessage For=@(() => Std.Age)/>
</div>
@*As the ValidationMessage component displays error messages for a single field
it requires us to specify the identity of the field
To ensure our parameters value is never incorrect Blazor requires us to
specify an Expression when identifying the field.
This means to specify the identity of the field we should use a lambda expression*@
<div class="form-group">
<label for="Birthdate">Birthdate:</label>
<InputDate @bind-Value=Std.Birthdate class="form-control" id="Birthdate" />
</div>
<div class="form-group">
<label for="BackgroundInfo">Background Info:</label>
<InputTextArea @bind-Value=Std.BackgroundInfo class="form-control" id="BackgroundInfo" />
<ValidationMessage For=@(() => Std.BackgroundInfo) />
</div>
<div class="form-group">
<label for="Cookies">Accept Cookies:</label>
<InputCheckbox @bind-Value=Std.Cookies class="form-control" id="Cookies" />
</div>
<input type="submit" class="btn btn-primary" value="Save"/>
</EditForm>
... and finally once all errors are clear (OnValidSubmit instead of OnSubmit) we can direct our code to execute any business logic we wish, in this case the FormSubmitted method
<EditForm Model="@Std" OnValidSubmit="@FormSubmitted">
Offer you the challenge to create a simple Blazor application which models a Guestbook .
In this Lecture we will
Work through the solution to the previous lecture challenge problem ... Guestbook
note the use of OnValidSubmit and OnInvalidSubmit
note the use of class="alert @StatusClass
note the use of <button type="submit">OK </button>
The OK button triggers a form submit … whereas previously it may have called a method directly via @onclick=”HandleValidSubmit” we now rely on the EditForm to tell us where to go after the form data has passed all validations
@page "/guestform"
@using GuestBook.Models
<h1>Udemy Course Guest Book </h1>
<p>Leave me a message</p>
<EditForm Model="@Model" OnValidSubmit="@HandleValidSubmit" OnInvalidSubmit="@HandleInvalidSubmit">
<div class="alert @StatusClass">@StatusMessage</div>
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group">
<label for="name">Name: </label>
<InputText Id="name" Class="form-control" @bind-Value=Model.Name>
</InputText>
<ValidationMessage For=@(() => Model.Name) />
</div>
<div class="form-group">
<label for="body">Text: </label>
<InputTextArea Id="body" Class="form-control" @bind-Value=Model.Text>
</InputTextArea>
<ValidationMessage For=@( () => Model.Text) />
</div>
@*The OK button triggers a form submit … whereas previously it may have called
a method directly via @onclick=”HandleValidSubmit” we now rely on the
EditForm to tell us where to go after the form data has passed all validations*@
<button type="submit">OK</button>
</EditForm>
@code {
private string StatusMessage;
private string StatusClass;
private GuestbookEntry Model = new GuestbookEntry();
protected void HandleValidSubmit()
{
StatusClass = "alert-info";
StatusMessage = DateTime.Now + " Handle valid submit";
}
protected void HandleInvalidSubmit()
{
StatusClass = "alert-danger";
StatusMessage = DateTime.Now + " Handle invalid submit";
}
}
Offer you a second Challenge
You are to revise the Birthday Problem Application from Lecture93 so that it implements Form Validation
The Lecture 93 solution is located in the resources for your convenience
BdayProb.rar (Lect93BdayProb/DateOfBirthValid)
First we must create a class referencing the Date of Birth
public class Student
{
[Required]
public string DofB { get; set; }
}
Next we create a Razor Component called BirthPage.razor
In the Code section we make an instance of our class to use in the EditForm and we create our Calculate method
Student Std = new Student();
private string DayOfWeek = null;
private void Calculate()
{
var isValid = DateTime.TryParse(Std.DofB, out var date);
if (isValid)
{
DayOfWeek = date.DayOfWeek.ToString();
}
}
The HTML section
@page "/birthpage"
@using DateOfBirthValidated.Models
<h4>What's your date of birth? </h4>
<EditForm Model="@Std" OnValidSubmit="@Calculate">
<DataAnnotationsValidator />
<ValidationSummary />
<label for="dofb">Date of Birth m/d/yr</label>
<InputText id="dofb" @bind-Value=Std.DofB class="form-control" />
<button type="submit" >Submit</button>
</EditForm>
@if (DayOfWeek != null)
{
<p>You were born on a ... @DayOfWeek</p>
}
In this Lecture we will
Create a simple Blazor Server Application which implements validations for a COVID Vaccination Sign Up form. This example serves as a review and extension of many of the concepts covered so far on Form Validations.
CovidServerApp
First we create our User class and include our data annotations right away
public class User
{
//This property represents the user id and will be auto-assigned
//when the User model is saved in the database (in theory)
public int Id { get; set; }
public string FirstName { get; set; }
[Required(ErrorMessage = "Last name is required")]
[StringLength(20,MinimumLength =3,ErrorMessage = "No fewer than 3 and no more than 20 letters")]
public string LastName { get; set; }
[Required]
[StringLength(9, MinimumLength = 9, ErrorMessage = "Must be 9 digits")]
public string SIN { get; set; }
[Required]
[Range(typeof(DateTime),"1-1-2000","12-31-2021",ErrorMessage ="Date must be between 1-Jan-2000 and 31-Dec-2021")]
public DateTime DateOfBirth { get; set; }
[Required(ErrorMessage ="Email is required")]
[DataType(DataType.EmailAddress)]
[EmailAddress]
public string Email { get; set; }
}
Note the new Data Annotations for SIN, DateOfBirth and Email
Then we code our UI in the index.razor page
we have created a new User object called "NewUser" in the code section , this property is used to bind the Model attribute of the EditForm
User NewUser = new User();
bool displayValidationErrorMessages = false;
bool displayUserAddedToDB = false;
//this method is invoked when the user clicks on the
//Add user button and the Model is in a valid state
//Here we are just updating boolean properties to display messages
//on the UI
private void HandleValidSubmit()
{
displayValidationErrorMessages = false;
displayUserAddedToDB = true;
}
private void HandleInvalidSubmit()
{
displayValidationErrorMessages = true;
displayUserAddedToDB = false;
}
OnValidSubmit invokes HandleValidSubmit when the user clicks Add button and Model is in a valid state
OnInvalidSubmit invokes HandleInvalidSubmit when user clicks Add and the Model is in an invalid state.
<div class="container">
<EditForm Model="@NewUser" OnValidSubmit="@HandleValidSubmit" OnInvalidSubmit="HandleInvalidSubmit">
@*This component is added in order to add validation support*@
<DataAnnotationsValidator />
<div class="row">
<div class="col-md-8">
<div class="row" style="margin-top:10px">
<div class="col-md-12">
<label for="firstName">First Name </label>
<InputText id="firstName" @bind-Value=NewUser.FirstName class="form-control" />
<ValidationMessage For=@(()=>NewUser.FirstName) />
</div>
</div>
..............
<button type="submit" class="btn btn-info" Style="margin-top:10px">Sign Up</button>
</div>
@if (displayValidationErrorMessages)
{
<div class="col-md-4" style="margin-top:10px">
<label>Validation Messages:</label>
<ValidationSummary/>
</div>
}
</div>
</EditForm>
@*Added message here just to mimic validation has passed and some processing
has happened*@
@if (displayUserAddedToDB)
{
<div class="row bg-success text-white" style="margin-top:10px; height:40px">
<label class="p-2">User added to database ...</label>
</div>
}
</div>
CovidServerAppUpdated
Updated User class property DateOfBirth to default to current date ... so now calendar (InputDate) will start off pointing the current date when the app is run
public DateTime DateOfBirth { get; set; } = DateTime.Now;
Modified the UI so that FirstName and LastName InputText are side by side and so are SIN and DateOfBirth
<div class="col-md-6">
<label for="firstName">First Name </label>
<InputText id="firstName" @bind-Value=NewUser.FirstName class="form-control" />
<ValidationMessage For=@(()=>NewUser.FirstName) />
</div>
<div class="col-md-6">
<label for="lastname">Last Name </label>
<InputText id="lastname" @bind-Value=NewUser.LastName class="form-control" />
<ValidationMessage For=@(()=>NewUser.LastName) />
</div>
Offer you the challenge to implement a form validation within the Lecture 111 Blazor Application called Lect111BlazorSongList4UpdateEmptyAddEditCheck
BlazorSongLIstValidatedAddUpdateCheck
For demonstration purposes I only modified the Add form to implement an EditForm
@if (songs != null && mode == MODE.Add) // Insert form using EditForm
{
<EditForm Model="@sg" OnValidSubmit="Insert">
<DataAnnotationsValidator/>
<ValidationSummary/>
<label>Title: <InputText @bind-Value="sg.Title" class="form-control"/></label>
<br />
<label>Artist: <InputText @bind-Value="sg.Artist" class="form-control" /></label>
<br />
<label>Year: <InputText @bind-Value="sg.Year" class="form-control"/></label>
<br />
<button type="submit" class="btn btn-warning mt-3 mr-3">Insert</button>
<button @onclick="Cancel" class="btn btn-secondary mt-3">Cancel</button>
</EditForm>
}
Song sg = new Song();
protected async Task Insert()
{
Song s = new Song()
{
SongId = Guid.NewGuid().ToString(),
Title = sg.Title, //can't just use title, artist, year
Artist = sg.Artist,//variables that were here previously
Year = sg.Year
};
//This check is actually NOT required now because our Add UI uses EditForm
//which knows about the REQUIRED fields in our Song class
if (!string.IsNullOrEmpty(sg.Title) && !string.IsNullOrEmpty(sg.Artist))
{
await songService.InsertSongAsync(s);
ClearFields();
await load();
mode = MODE.None;
}
}
In this Lecture we will
Create a more sophisticated Registration type form (Registration) which implements among other things
A drop down list for name title (Mr./Mrs/Miss/Ms)
A confirm password check
An "Accept Terms" checkbox
The use of EditContext. Its the way EditForm keeps track of the current state of the form (which fields have been modified) and any validation errors which have been triggered. You can use it instead of the Model attribute assignment.
Storing the entered information in JSON format by calling the JsonSerializer and re-displaying it with a simple alert command called from a Javascript function.
Start by creating our Reg class
public class Reg
{
[Required]
public string Title { get; set; }
[Required]
[Display(Name = "First Name")] //This forces validation to use First Name instead
public string FirstName { get; set; } //of FirstName if Validation Error Message needs
//to be displayed
[Required]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[Display(Name = "Date of Birth")]
public DateTime? DateOfBirth { get; set; }
//The question mark turns it into a nullable type
//which means that either it is a DateTime object or it is null
// DateOfBirth is now of type Nullable<DateTime> rather than DateTime
// now calendar defaults to current date
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[MinLength(6, ErrorMessage = "Password must be at least 6 characters")]
public string Password { get; set; }
[Required]
[Compare("Password")]
[Display(Name = "Confirm Password")]
public string ConfirmPassword { get; set; }
[Required]
[Range(typeof(bool), "true", "true", ErrorMessage = "Required")]
public bool AcceptTerms { get; set; }
}
Note the use of [Display(Name="First Name")] ... this forces validation to use First Name instead of FirstName if Validation Error Message needs to be displayed.
Note the user of [Compare("Password")]
Note use of [Range(typeof(bool), "true","true"
Next we start work on our RegForm.razor file
First we add some required references at the top of the file ... the last two are required to create our JSON text file which will list all the information we have entered
@page "/regform"
@using Registration.Models
@using System.ComponentModel.DataAnnotations
@using System.Text.Json
@inject IJSRuntime JSRuntime;
Note in this EditForm we don't use the Model attribute. In this application we want more direct control Under the hood. EditForm keeps track of the current state of the form (which fields have been modified) and any validation errors which have been triggered. It stores this in something called an EditContext. So we have dropped the Model attribute assignment and swapped it for an EditContext instead. You can specify either a Model or EditContext but not both. When you assign a model using the Model attribute your EditForm will create and manage its own EditContext. Conversely when you assign your own EditContext you need to create it yourself. See the code section below to see how we create our own EditContext to make this work
Also note the use of @onreset in the EditForm declaration and the corresponding button type=reset to Cancel the form entry
<EditForm EditContext="@editContext" OnValidSubmit="HandleValidSubmit" @onreset="HandleReset">
<div class="text-center">
<button type="submit" class="btn btn-primary mr-1">Register</button>
<button type="reset" class="btn btn-secondary">Cancel</button>
</div>
Let's focus on the Code section before we enter all the HTML
@code {
private Reg model = new Reg();
private EditContext editContext;
//Note how we point our new EditContext to an instance
//of our Model called model when we instantiate it
//Now you can access editContext to trigger validation
//and check if anything has been modified
protected override void OnInitialized()
{
editContext = new EditContext(model);
}
private void HandleValidSubmit()
{
//converts a .Net object to a JSON string
//Indented tells Json.Net to serialize the data with indentation and new lines.
//If you don't do that, the serialized string will be one long string with no indentation
var modelJson = JsonSerializer.Serialize(model, new JsonSerializerOptions { WriteIndented = true });
JSRuntime.InvokeVoidAsync("displayAlert", modelJson);
}
private void HandleReset()
{
model = new Reg();
editContext = new EditContext(model);
}
}
We make a call to a Javascript function "displayAlert" which we must create
Go into the wwwroot directory and create a folder called js
Right click on the js folder and create a Javascript file called myscript.js
Create the displayAlert function
function displayAlert(modelJ) {
alert("SUCCESS!!" + modelJ);
}
Make sure to update the index.html page so Blazor knows where to find this new javascript file
Now we can complete the UI
Supplementary Demo (.NET 9 update)
RegistrationNET9
Note in the link "Registration Form App", the addition of the InputNumber component with type=slider .... But unlike the input html tag where we could add oninput for a real time update effect , this does not work now.
Basically bind-Value:event="oninput" is not supported in InputNumber or any other Blazor EditForm Component, but you can easily derive a new control from 'InputNumber' which does update on input.
You create a new Razor Component ... called MyInputNumber.razor , have it inherit Forms.InputNumber and then use the basic html input with additional attributes ... See the RegForm.razor page and MyInputNumber.razor page for all the details
MyInputNumber.razor
@inherits Microsoft.AspNetCore.Components.Forms.InputNumber<int>
<input type="range" max="200" min="10" step="10" @attributes="@AdditionalAttributes" class="@CssClass" @bind="@CurrentValueAsString" @bind:event="oninput" />
RegForm.razor
<div class="form-group row">
<div class="form-group col-5">
<label class="mt-2">Age ... using InputNumber:@(model.Age == 0 ? "" : model.Age)</label>
<InputNumber type="range" max="100" min="1" step="1" @bind-Value="model.Age" />
<ValidationMessage For="@(() => model.Age)" />
</div>
<div class="form-group col-5">
<label class="mt-2">Age ... using MyInputNumber:@(model.Age == 0 ? "" : model.Age)</label>
<MyInputNumber @bind-Value="model.Age" />
<ValidationMessage For="@(() => model.Age)" />
</div>
</div>
RegNET9InputNumberOninputTimeLect125
Note the addition of the time picker (MyInputTime) in the Registration Form App. The HTML input type=time is not available in Blazor Edit Components
RegNET9MarkdownEditorLect125
In this update ... Note the addition of Simple Markdown Editor in the Registration Form App. SimpleMDE ... a simple and embeddable Javascript editor... You may need to run it without the Debugger (Ctrl+ F5) to see it in action)
Here are the Details
In the wwwroot folder we have updated the index.html file to point to the js code and stylesheet
In the wwwroot folder we have added a javascript file blazor-simplemde.js
In the Pages folder we have added a new Razor Component called BlazorSimpleMde.razor
In this BlazorSimpleMde.razor file we have added a textarea element and a code block to initialize the markdown editor and subsequently get the value (contents) of the editor
... and finally we add BlazorSimpleMde component (the Markdown Editor) to our form
FormValidDisplayNameNET9
This is a simple demo application which reviews and extends our knowledge of how to use forms and validations in Blazor WebAssembly.
Click on the Forms and Validations link in the navigation bar to see the demo.
... Features to note are:
DisplayName="Car Drivers Age" hard coded right in the InputNumber component
The Addition of some SEO (Search Engine Optimization ... Check out the index.html file in the wwwroot folder
The use of C# conditional preprocessor directives and predefined constants you can use to check the Blazor target framework
DataAnnotationRegularExpression
This is a simple application that demonstrates how to use Data Annotations and Regular Expressions in Blazor.
Click on the Country Entry Page link in the navigation menu to see how we limit the country entry on the form to characters (alphabet) and numbers, no special characters are allowed
Check out the Models folder (Countries.cs class) for the specific details. There we use the RegularExpression attribute that validates the property value against a regular expression pattern so that only letters and numbers are allowed.
Want to take a deeper dive into Regular Expressions
https://www.programiz.com/csharp-programming/regex
Supplementary Demo (Enrichment)
ClockChallengeBlazorNET8Wasm
This is a graphical solution to a classic Programmer Interview question ... Calculate the angle of the hour and minute hands for any given time
Incorporate a simple class for the hours and minutes
public class PageModel
{
[Required]
[Range(0,12,ErrorMessage ="Only values between 0-12")]
public int hour { get; set; } = 3;
[Required]
[Range(0,59,ErrorMessage ="Only values between 0-59")]
public int minute { get; set; } = 30;
}
The UI uses an EditForm implementing EditContext
In the code section the GetAngle method does all the math to determine the angle between the hour and minute hand
The "cool" clock graphics part comes from CSS code from the site CodeFoundry
In this Lecture we will
Look at the Authentication System Overview
UI (Register/Login)
Functionality (Authentication/Authorization)
Data Storage
Discuss the difference between Authentication and Authorization
Authentication is the process of determining if someone is who they claim to be
most common way is a username and password check
another example of authentication is using your pin code with a credit card
Authorization is the process of checking if someone has the rights to access a resource (what the user can and cannot do)
Authorization occurs after an identity has been established via authentication and determines what parts of a system you can access. For example, if you have administrator rights on a system you can access everything. But if you're a standard user, you may only be able to access specific screens.
Create a new server-side Blazor application with authentication enabled (BlazorAuthPart1)
Choose Individual User Accounts
Notice that a Data folder exists with Migrations set up BUT not applied yet and in appsetttings.json a connection string has already been created ready for migration and database creation
change the connection string name to BlazorAuth
Once app has been updated press F5 to run
Click the Register link in the top right and fill in your details and press Register
When you access membership pages for the first time and the database is not setup, the system asks for migration.
Next Click on Apply Migrations
This will create the SQL database and tables related to identity framework.
Among the tables created we will use “AspNetUsers” table for storing user information. We will use “AspNetRoles” table for storing role information. We will also use “AspNetUserRoles” table to store role details for a user.
You will then be redirected back to the home page as an authenticated user. The Register link has been replaced with your email address and log out button
Let's now look at several simple techniques to secure Blazor components
Block access to Counter and FetchData Page for users not logged in
@page "/counter"
@attribute [Authorize]
@*Blocks access to Counter page if you are not logged in*@
Using the AuthorizedView component
In Blazor we use AuthorizeView component to show or hide UI elements depending on whether the user is authorized to see it.
In this example, AuthorizeView component is used in it's simplest form, without any parameters (i.e roles or policies), so, it only checks if the user is authenticated.
If the user is authenticated, then the content in <Authorized> component is displayed, otherwise, the content in <NotAuthorized> component is displayed.
<AuthorizeView>
<Authorized>
This content is displayed only if the user is Authorized
</Authorized>
<NotAuthorized>
This content is displayed if the user is Not Authorized
</NotAuthorized>
</AuthorizeView>
Lets block access to specific parts of the Counter page for users not logged in
... don't need @attribute[Authorize]
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<AuthorizeView>
<Authorized>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</Authorized>
<NotAuthorized>
You are not authorized to access counter button
</NotAuthorized>
</AuthorizeView>
Providing a global authorized view ... App.razor
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" >
<NotAuthorized>
You don't have permission to access page contact admin'
</NotAuthorized>
</AuthorizeRouteView>
Note change in FetchData page
Supplementary Demos (same app different .NET version)
BlazorAuthPart1NET6
BlazorAuthPart1NET6toNET7
In this Lecture we will
Take a deeper look at Authorization with Roles and utilize the IdentityManager Utility (BlazorAuthPart2)
Role based authorization in Blazor
Uses the Roles parameter.
<AuthorizeView Roles="administrator">
<p>Displayed if the logged in user is in administrator role</p>
</AuthorizeView>
First we need to modify the “ConfigureServices” method in the “Startup” class with the changes below so that we can control the authorization with identity roles in the application.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();
services.AddSingleton<WeatherForecastService>();
}
Using .NET 6 or higher we modify the Program.cs file instead
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
Next we are going to load up the Blazor Utility App IdentityManager into Visual Studio. This application will allow us to assign roles with a nice UI instead of deep diving into the LocalDB Tables
Before we can use it we must make two modifications
First we must change Startup reference of .UseSqlite to .UseSqlServer
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
Second in appsettings.json need to change connection string to match one used in current app
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=BlazorAuth;Trusted_Connection=True;MultipleActiveResultSets=true"
}
Now execute app and let it connect to your local db within the BlazorAuthPart2 project
Using .NET 7
Then implement IdentityManager Utility Application (CarlFranklin Version) ... works with .NET 8 also
Remember to update the appsetting.json Database file to match the Database name in our Application (BlazorAuth) and make the Server App the startup Project.
Now we can startup our main again and recap a couple of key features so far
If we don't log in we have limited access to the Counter.razor page through our use of <AuthorizeView> and no access to the FetchData.razor page
If we log in we have total access to every page.
Let's now start creating restrictions for authenticated users
First we will restrict what menu options the user sees
We go into NavMenu.razor and add/modify
<AuthorizeView Roles="CanViewCounter">
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</li>
</AuthorizeView>
<AuthorizeView Roles="CanViewWeather">
<li class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</li>
</AuthorizeView>
Here we are using <AuthorizeView> with the Roles parameter
Next we need to add these Role names to our DB via the IdentityManager Utility and then assign some/all of these roles to specific users.
First we create our Roles CanViewCounter and CanViewWeather
Lets assign our user the CanViewCounter role but NOT the CanViewWeather role
Discuss the loop hole... can still hard code uri/url ... /fetchdata and get to the non authorized page
Need to add the Authorize attribute to the fetchdata page
@attribute [Authorize(Roles = "CanViewWeather")]
Let's now further restrict parts of the UI of specific pages referenced in the NavMenu
In the Counter.razor page we will restrict use of the button
<AuthorizeView Roles="CanClickCounterButton">
<Authorized>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</Authorized>
<NotAuthorized>
You are not authorized to click counter button
</NotAuthorized>
</AuthorizeView>
Make sure to add this new role CanClickCounterButton in the IdentityManager Utility ... then assign this role to user
Lastly we are going to use code to determine if a user is in a specific role (code based authorization)
First we need to go to _imports.razor and add
@using System.Security.Claims
@*above using added to allow authorization checking in code as opposed to UI*@
Next we add a new role called CanIncrementCounter and assign it to the user who also already has the CanClickCounterButton role.
Then in the Counter.razor page where we will be implementing the code we inject the statement below
@inject AuthenticationStateProvider AuthenticationStateProvider
In the Code Section
private ClaimsPrincipal User;
private string Message = "";
private int currentCount = 0;
private void IncrementCount()
{
Message = "";
if User.Identity.IsAuthenticated && User.IsInRole("CanIncrementCounter")
{
currentCount++;
}
else
{
Message = "You do not have permission to increment the counter !";
}
}
protected async override Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
User = authState.User;
}
In this Lecture we will
Learn how to customize Identity pages like the login page (BlazorAuthPart3)
Learn that when you display the login page it points to Identity/Account/Login but this page isn't visible in the Solution Explorer
To get access to these Identity pages you need to add a bit of scaffolding
Right click on the project and choose New Scaffolding Item
Choose Identity from the list on the left and then Add
Here you can choose from dozens of files to override. For our example we will just override the login page
This creates a new file in the Blazor server Identity area called Login.cshtml (Area/Identity/Pages/Account)
.NET 6 Update ... Program.cs (see BlazorAuthPart3NET6)
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("BlazorAuthPart1NET6ContextConnection") ?? throw new InvalidOperationException("Connection string 'BlazorAuthPart1NET6ContextConnection' not found.");
builder.Services.AddDbContext<BlazorAuthPart1NET6Context>(options =>
options.UseSqlServer(connectionString));;
//This AddDefaultIdentity was removed in .NET 6 ... to solve Error
//"InvalidOperationException:Scheme already exists (app.Run() )
//This occurred during the creation of the Identity Area using Scaffolding
//************************************************************************
//builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
// .AddEntityFrameworkStores<BlazorAuthPart1NET6Context>();;
// Add services to the container.
//.NET 6 Scaffolding issue
// connectionstring conflicted with declaration above so was
//changed to connectionString1
var connectionString1 = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString1));
Modify the Login.cshtml page (located in the Area/Identity/Pages/Account folder)
Remove the right side contents
<div class="col-md-6 col-md-offset-2"> ...
Update the Title on the top left corner (not the header section ... just below)
ViewData["Title"] = "Chiarelli Udemy Login";
<h1 style="font-weight:bold; color:green">@ViewData["Title"]</h1>
<h5 style="font-style:italic; color:red">Use a local account </h5>
Add an image (make an img folder in the wwwroot folder) ... image will be on the left side column and email password login on the right column
<div class="row">
<div class="col-md-4">
<img src="/images/logos.jpg" width="250" height="200" />
</div>
<div class="col-md-8">
Some of the information displayed on the Login page cannot be modified on this page directly and is associated with a layout page ... but where is this layout page?
First we look in _ViewStart.cshtml
@{
Layout = "/Pages/Shared/_Layout.cshtml";
}
... this leads us to _Layout.cshtml located in Pages/Shared
Now we can cleanup/modify elements of Layout.cshtml page
We can modify the Header (same line as header and login)
<a class="navbar-brand" style="font-weight:bold; color:purple" href="~/">Blazor Authentication Demo</a>
We can modify the Footer
<footer class="footer border-top text-muted">
in the footer area
<footer class="footer border-top text-muted">
<div class="container">
© 2021 - A Gentle Introduction to ASP.NET - <a asp-area="" asp-page="Privacy">Privacy</a>
</div>
</footer>
In this Lecture we will
Learn how to implement authentication and authorization in a Blazor Server App using an external login via Facebook
Start off and create a basic Blazor Server Side App with Authentication (FacebookAuthentication)
modify appsettings.json and change the connection string to BlazorFace
then open the Package Manager Console and perform migration
Update-Database
then Right Click on the project and select properties
select Debug and note SSL URL -> https://localhost:44354 ... note: your number will be different
Using Visual Studio 2022 .NET 6 or greater
select Debug ... General ... Open debug lauch profiles UI ... Go down to App URL and pick https://localhost:7019
Learn how to set up a Facebook Developers account
first you must sign into regular Facebook account
then go to Facebook Developers Site and register as a developer
https://developers.facebook.com/docs/development/register
... once registered and logged in click on My Apps
Now create a Facebook App
Click on "Create App" and choose "What do you want to do ?" .... Choose Other ... App Type ... Consumer
Put in the "Display Name" ChiarelliBlaz and "Contact email" ... click on Create App
Under the “Add Products to Your App” section, select the “Facebook Login” product and click on “Set Up” button
A QuickStart wizard will be launched asking you to select a platform for the app ... choose Web
Under the site URL field enter the specifics of your application with /signin-facebook appended to it. For this lecture, the URL will be https://localhost:44354/signin-facebook. Click on “Save” button.
Now click on Settings on the left hand side > Basic on the navigation menu. You will see the App ID and App Secret values. Make a note of these values as we need them to configure Facebook authentication in our Blazor app
Next we return back to Visual Studio where we now need to install the Facebook authentication middleware NuGet Package
Go here for all the details
https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Facebook/3.1.14
From the Program Manager Console type
Install-Package Microsoft.AspNetCore.Authentication.Facebook -Version 3.1.14
OR ... go to Manage NuGet Packages Solution and Browse search for Microsoft.AspNetCore.Authentication.Facebook and install there
Using .NET 6
pick ... NuGet Microsoft.AspNetCore.Authentication.Facebook 6.0
Now we need to Configure the server-side Blazor app to use Facebook authentication
We need to store the App ID and App Secret field values in our application. We will use Secret Manager tool for this purpose. The Secret Manager tool is a project tool that is used to store secrets such as password, API Key, etc. for a .NET Core project during the development process.
Open our web application once again and Right-click the project in Solution Explorer. Select Manage User Secrets from the context menu. A secrets.json file will open. Put the following code in it.
{
"Authentication:Facebook:AppId": "Your Facebook AppId",
"Authentication:Facebook:AppSecret": "Your Facebook AppSecret"
}
Now open Startup.cs file and put the following code into ConfigureServices method. (See ConfigureServicesAddition.txt)
services.AddAuthentication().AddFacebook(facebookOptions =>
{
facebookOptions.AppId = Configuration["Authentication:Facebook:AppId"];
facebookOptions.AppSecret = Configuration["Authentication:Facebook:AppSecret"];
facebookOptions.Events = new OAuthEvents()
{
OnRemoteFailure = loginFailureHandler =>
{
var authProperties = facebookOptions.StateDataFormat.Unprotect(loginFailureHandler.Request.Query["state"]);
loginFailureHandler.Response.Redirect("/Identity/Account/Login");
loginFailureHandler.HandleResponse();
return Task.FromResult(0);
}
};
});
Using .NET 6 or greater then you need to update Program.cs (See FacebookAuthNET6 ... ConfigureServicesAdditionNET6.txt)
builder.Services.AddAuthentication().AddFacebook(facebookOptions =>
{
facebookOptions.AppId = builder.Configuration["Authentication:Facebook:AppId"];
facebookOptions.AppSecret = builder.Configuration["Authentication:Facebook:AppSecret"];
facebookOptions.Events = new OAuthEvents()
{
OnRemoteFailure = loginFailureHandler =>
{
var authProperties = facebookOptions.StateDataFormat.Unprotect(loginFailureHandler.Request.Query["state"]);
loginFailureHandler.Response.Redirect("/Identity/Account/Login");
loginFailureHandler.HandleResponse();
return Task.FromResult(0);
}
};
});
Note the OAuthEvents() namespace error ... must add using statement
using Microsoft.AspNetCore.Authentication.OAuth;
OAuth, which stands for “Open Authorization,” allows third-party services to exchange your information without you having to give away your password
Do a quick test run to check for errors, and observe that right now entire site is accessible to everyone
Add some Authorization by modifying the Counter.razor page
@attribute [Authorize]
Test out the demo again using your Facebook login
the first time through you will need to go through a Facebook registration/association process
... after logging in view the SQL Server Object Explorer and look at the contents in ASPNet.User.Logins
Offer you the challenge to add Google Authentication
In this lecture we will
Learn What is Auth0?
Auth0 is a secure and universal service which provides authentication and authorization functionality. It works on the basis of tokens and works with different identity providers.
With this approach, whenever you need to login to your app, you redirect the user to Auth0 (an external login provider) to do the actual sign-in. Once the user has signed in, they're redirected to a callback page in your app. Your app then talks directly to Auth0 to obtain the authentication details. Auth0 off loads the responsibility of managing the user accounts yourself,
Learn why Auth0 is a good solution for Social Authentication
There are a lot of social networks: Google, Facebook, Twitter. And all these services provide their own systems of authentication . To avoid the need to add authentication for each option separately, Auth0 offers a convenient solution which will simplify the whole process.
Learn the key benefits of Auth0
Security
Auth0 security is provided by the OAuth 2.0 authentication protocol which allows the application to grant access rights to the user's resources on another service. The protocol eliminates the need to trust the login and password to the app.
UI options
Auth0 provides the ability to use both built-in and custom UI.
Auth0 Analytics
Auth0 offers effective tools to track users on a website or in an application. Integration Auth0 Analytics allows capturing and measuring specific events, such as:
the number of new and existing users;
the number of users registered in each application;
in-app login activity in the past year;
the number of new registrations during the current day;
logins and new registrations during the last week;
identity providers used to log-in to the application.
Demonstrate how to create an Auth0 account
You can signup for Auth0 for free at https://auth0.com/signup. The free plan is valid for up to 7,000 active users, so is a great option for getting started. For features such as custom domains, role management, and more active users, you'll need to look at one of the paid plans.
Work through securing a simple Blazor Server App which integrates with the Auth0 service (Auth0ServerApp1 ) ... completed file located in NEXT Lecture
Create a default Blazor Server Application with no Authentication enabled
Execute app and make note of the localhost address => https://localhost:5001
... OR goto App Properties/Debug/App URL
Access the Auth0 Dashboard in order to create your Auth0 application.
Once in the dashboard, move to the Applications section and follow these steps:
Click on Create Application
Provide a friendly name for your application (for example, Intro Auth0 Blazor Server App) and choose Regular Web Applications as an application type
Click the Create button ... then choose ASP.NET Core
These steps make Auth0 aware of your Blazor application and will allow you to control access.
After the application has been created, move into the Settings tab and take note of your Auth0 domain, your client id, and your client secret.
pop-up notepad and paste the values in
Then, in the same form, assign the value https://localhost:5001/callback to the Allowed Callback URLs field
... and the value https://localhost:5001/ to the Allowed Logout URLs field.
The first value tells Auth0 which URL to call back after the user authentication. The second value tells Auth0 which URL a user should be redirected to after their logout.
NOTE: The numbers will be different for your local machine
Click the Save Changes button to apply them.
In this Lecture we will
Begin the process of Configuring our Blazor App to integrate with Auth0
Open appsettings.json and add to the AllowedHosts section
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"Auth0": {
"Domain": "dev-qbokf8gs.us.auth0.com",
"ClientId": "5EZ8BZk30JOi8L6TD6MybkB6WHWJzhSa",
"ClientSecret": "9CensTG1v-2k7XiEiPl6dxKOaxOH9PtsFl2RzkM89lRSsRGh5hCW4G674_D0PlrM"
}
}
Replace the Domain,ClientId and ClientSecret with the respective values taken from the Auth0 dashboard
Next install the Microsoft.AspNetCore.Authentication.OpenIdConnect library .... go into the Package Manager
Install-Package Microsoft.AspNetCore.Authentication.OpenIdConnect -Version 3.1.16
Now we modify Startup.cs (... Pre .NET 6 ) to configure the required services, and add the authentication and authorization middleware. Most of this code is copied straight from the Auth0 site (https://auth0.com/docs/quickstart/webapp/aspnet-core)
update the using statements first
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.Cookies;
then update ConfigureServices() method. Here we are configuring the Blazor app in order to support authentication via OpenID Connect. The Auth0 configuration parameters are taken from the appsettings.json file
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
// Add authentication services
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect("Auth0", options =>
{
// Set the authority to your Auth0 domain
options.Authority = $"https://{Configuration["Auth0:Domain"]}";
// Configure the Auth0 Client ID and Client Secret
options.ClientId = Configuration["Auth0:ClientId"];
options.ClientSecret = Configuration["Auth0:ClientSecret"];
// Set response type to code
options.ResponseType = "code";
// Configure the scope
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile"); //new code for accessing user profile from Auth0
// Set the callback path, so Auth0 will call back to http://localhost:3000/callback
// Also ensure that you have added the URL as an Allowed Callback URL in your Auth0 dashboard
options.CallbackPath = new PathString("/callback");
// Configure the Claims Issuer to be Auth0
options.ClaimsIssuer = "Auth0";
options.Events = new OpenIdConnectEvents
{
// handle the logout redirection
OnRedirectToIdentityProviderForSignOut = (context) =>
{
var logoutUri = $"https://{Configuration["Auth0:Domain"]}/v2/logout?client_id={Configuration["Auth0:ClientId"]}";
var postLogoutUri = context.Properties.RedirectUri;
if (!string.IsNullOrEmpty(postLogoutUri))
{
if (postLogoutUri.StartsWith("/"))
{
// transform to absolute
var request = context.Request;
postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
}
logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}";
}
context.Response.Redirect(logoutUri);
context.HandleResponse();
return Task.CompletedTask;
}
};
});
services.AddHttpContextAccessor();
}
... and finally update the configure() method
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
//Auth0 update
//Adding the middleware to manage the cookie policy, the authentication
//and the authorization processes. Now the application has the
//infrastructure to support authentication
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
USING .NET 6 or higher ... we update the Program.cs file instead ... these modifications are somewhat shorter (see Auth0ServerNET6 )
using Auth0ServerNET6.Data;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Auth0.AspNetCore.Authentication;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
//new code for .NET 6 Auth0
builder.Services
.AddAuth0WebAppAuthentication(options => {
options.Domain = builder.Configuration["Auth0:Domain"];
options.ClientId = builder.Configuration["Auth0:ClientId"];
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
//new code for .NET 6 Auth0
app.UseAuthentication();
app.UseAuthorization();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
Now it's time to secure the server side. In order to prevent unauthorized users from accessing the server side functionalities you need to protect them. So we open the Index.razor component and add the Authorize attribute
@page "/"
@attribute [Authorize]
<h1>Hello, world!</h1>
Welcome to your new app.
This ensures that the server side rendering of your pages is triggered only by authorized users.
Add the same @attribute to the Counter.razor page
Next we create login and logout endpoints. Recall that the Blazor Server hosting model communicates between the client side and server side through SignalR not HTTP. Since Auth0 uses standard protocols like OPENID and OAuth that rely on HTTP, you need to provide a way to bring those protocols on Blazor
To solve this issue, you are going to create two endpoints login and logout that redirect requests for login and for logout to Auth0.
Here we need to create two Razor pages not Razor components in the Pages folder.
Login.cshtml .... and its associated Login.cshtml.cs file
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Auth0ServerApp.Pages
{
public class LoginModel : PageModel
{
//This code starts the challenge for the Auth0 authentication scheme
//we defined in Startup.cs
public async Task OnGet(string redirectUri)
{
await HttpContext.ChallengeAsync("Auth0", new AuthenticationProperties
{
RedirectUri = redirectUri
});
}
}
}
Using .NET 6
public async Task OnGet(string redirectUri)
{
var authenticationProperties = new LoginAuthenticationPropertiesBuilder()
.WithRedirectUri(redirectUri)
.Build();
await HttpContext.ChallengeAsync(Auth0Constants.AuthenticationScheme, authenticationProperties);
}
}
Logout.cshtml and its associated Logout.cshtml.cs file
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Auth0ServerApp.Pages
{
public class LogoutModel : PageModel
{
//This closes the users session on Auth0
public async Task OnGet()
{
await HttpContext.SignOutAsync("Auth0", new AuthenticationProperties { RedirectUri = "/" });
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
}
Using .NET 6
[Authorize]
public async Task OnGet()
{
var authenticationProperties = new LogoutAuthenticationPropertiesBuilder()
.WithRedirectUri("/")
.Build();
await HttpContext.SignOutAsync(Auth0Constants.AuthenticationScheme, authenticationProperties);
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
Next we need to secure the client side.
Here we have to implement some Authorization so that users see different content when they are logged in or not
Open the App.razor file and make the following modifications
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<Authorizing>
<p>Determining session state, please wait...</p>
</Authorizing>
<NotAuthorized>
<h1>Sorry</h1>
<p>You're not authorized to reach this page. You need to log in.</p>
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<p>Sorry, there's nothing at this address.</p>
</NotFound>
</Router>
</CascadingAuthenticationState>
Here you are using the AuthorizeRouteView component, which displays the associated component only if the user is authorized. In practice, the content of the MainLayout component will be shown only to authorized users. If the user is not authorized, they will see the content wrapped by the NotAuthorized component. If the authorization is in progress, the user will see the content inside the Authorizing component. The CascadingAuthenticationState component will propagate the current authentication state to the inner components so that they can work on it consistently.The AuthorizeRouteView component allows you to control access to the UI parts of your Blazor Application
Now we can run the app.
When a user tries to access your application, they will see the not authorized message
So, you need a way to let users authenticate (login/logout) . Lets create a razor component for this purpose by adding an AccessControl.razor file in the Shared folder with the following content:
// Shared/AccessControl.razor
<AuthorizeView>
<Authorized>
<a href="logout">Log out</a>
</Authorized>
<NotAuthorized>
<a href="login?redirectUri=/">Log in</a>
</NotAuthorized>
</AuthorizeView>
This component uses the Authorized component to let the authorized users see the Log out link and the NotAuthorized component to let the unauthorized users access the Log in link. Both links point to the endpoints you created before. In particular, the Log in link specifies the home page as the URI where to redirect users after authentication.
The final step is to add this component to the MainLayout .
Now our app is accessible just to authorized users. When a user clicks the Log in link they will be redirected to the Auth0 Universal Login page for authentication. After authentication completes they will be brought back to the home page of our app.
Learn how to access a user profile (Auth0ServerApp2)
Once you add authentication to your Blazor Server application, you may need to access some information about the authenticated user, such as their name and picture. You can do it by explicitly requesting the user profile from Auth0. This additional request is just a new scope of the OpenID Connect protocol
You simply add the profile scope to the OpenID Connect configuration in the ConfigureServices() method of the Startup class.
options.Scope.Add("profile");
Using . NET 6 ... no additions necessary
This change enables you to access the users data
Now that we have access to this data lets display some of this content in the Index.razor page
@page "/"
@inject AuthenticationStateProvider AuthState
@attribute [Authorize]
<PageTitle>Quiz Manager</PageTitle>
<h1>Welcome, @Username!</h1>
You can only see this content if you're authenticated.
<br />
<img src="@Picture">
@code {
private string Username = "Anonymous User";
private string Picture = "";
protected override async Task OnInitializedAsync()
{
var state = await AuthState.GetAuthenticationStateAsync();
Username = state.User.Identity.Name?? string.Empty;
Picture = state.User.Claims
.Where(c => c.Type.Equals("picture"))
.Select(c => c.Value)
.FirstOrDefault() ?? string.Empty;
await base.OnInitializedAsync();
}
}
Illustrate how Auth0 by default gives the user the login options of creating a login account on the Auth0 system or using a Google Account .
Demonstrate how to implement other social connections like Facebook
Go to "Getting Started" ... and choose "Add a Social Login Provider" ... Add Social Connection
Once created this connection can be added to any of the Applications in your Auth0 account
Go to your Applications and click on ... More Actions ... Connections
Activate your desired Connection
Demonstrate how to customize the look of the Auth0 Login page
Branding ... changing the image of the Universal Login screen ... Advanced Options ... Classic Login ... Company Logo
... OR in the Application settings ... Application Logo
Look through some the features of the Auth0 dashboard
Activity
User Management
Monitoring
In this Lecture we will
Recall how we have defaulted to .NET Core 3.1 as are target framework for most of our Blazor Apps
Learn that Blazor in .NET 5 includes many exciting updates and improvements that will make building your next Web app simpler and more productive.
Blazor WebAssembly in .NET 5 runs about 30% faster than Blazor WebAssembly 3.1. This performance boost is mainly due to optimizations in the core framework libraries, and improvements to the .NET IL interpreter. Things like string comparisons and dictionary lookups are generally much faster in .NET 5 on WebAssembly.
Less JavaScript, More C#
In .NET 5, Microsoft has added some new built-in features that reduce or eliminate the amount of JavaScript interop code required for some common scenarios.
Sometimes you need to set the focus on a UI element programmatically. Blazor in .NET 5 now has a convenient method on ElementReference for setting the UI focus on that element.
Blazor now offers an InputFile component for handling file uploads.
New InputRadioGroup and InputRadio components
You can further optimize your Blazor Web UI by taking advantage of the new built-in support for virtualization. Virtualization is a technique for limiting the number of rendered component to just the ones that are currently visible, like when you have a long list or table with many items and only a small subset is visible at any given time.
Highlight some of these new .NET 5 updates via some simple Blazor Server Apps.
BlazorNet5Examples (SetFocus/InputFile/InputRadioGroup)
Setting the focus on a UI element programmatically
@page "/focus"
<h3>Set UI focus without Javascript</h3>
<button class="btn btn-primary" @onclick="() => input.FocusAsync()">Focus the input!</button>
<input @ref="input" />
<input @ref="newinput" />
@code {
ElementReference input;
ElementReference newinput;
}
InputFile component for handling file uploads
The InputFile component is based on an HTML input of type “file”. By default, you can upload single files, or you can add the “multiple” attribute to enable support for multiple files. When one or more files is selected for upload, the InputFile component fires an OnChange event and passes in an InputFileChangeEventArgs that provides access to the selected file list and details about each file.
@page "/fileupload"
<h3>File Upload</h3>
<InputFile OnChange="OnInputFileChange" multiple />
@*Note: new InputFile component has file count built in
... so this is shown for comparison *@
Number of Files Selected : @numFiles
<div class="image-list">
@foreach (var imageDataUrl in imageDataUrls)
{
<img src="@imageDataUrl" />
}
</div>
@code {
List<string> imageDataUrls = new List<string>();
int numFiles = 0;
//InputFileChangeEventArgs provides access to the selected file list
//and details about each file
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
var imageFiles = e.GetMultipleFiles();
//IBrowserFile imageFiles = e.GetMultipleFiles();
//determine number of files selected
numFiles = imageFiles.Count;
//note: format is used by RequestImageFileAsync
//the command attempts to convert the current image to the
//specified file type (png ... in this case)
//with a max dimension of 100x100
var format = "image/png"; //mime type
foreach (var imageFile in imageFiles)
{
var resizedImageFile = await imageFile.RequestImageFileAsync(format, 100, 100);
//Initialize byte array to capture the byte data of the
//image stream. Size of the byte array needs to be declared
//as size of the uploaded file
//ReadAsync extension populates the buffer byte array
var buffer = new byte[resizedImageFile.Size];
await resizedImageFile.OpenReadStream().ReadAsync(buffer);
//Here the preview URL is assigned with a base64 string image format using string interlopation.
//This is called a data URI ... see https://www.dotnetperls.com/tobase64string for more info.
//In computer science, Base64 is a group of binary-to-text encoding schemes
//that represent binary data in an ASCII string format
//Base64 is most commonly used to encode binary data such as images, or sound files
//for embedding into HTML, CSS, EML, and other text documents.
var imageDataUrl = $"data:{format};base64,{Convert.ToBase64String(buffer)}";
imageDataUrls.Add(imageDataUrl);
//Alternate
//var imageDataUrl= "data:" + format + ";base64,"+Convert.ToBase64String(buffer);
}
}
}
Supplementary Demos ( ... related to InputFile)
TheBlazorFileLoadDetails
Simple implementation of InputFile ... after any file is chosen the FileName, FileSize, FileType and LastModfied date details are displayed
<InputFile OnChange="FileUpload"/>
public async Task FileUpload(InputFileChangeEventArgs e)
{
var browserFile = e.File;
if (browserFile!=null)
{
FileSize = browserFile.Size;
FileType = browserFile.ContentType;
FileName = browserFile.Name;
LastModified = browserFile.LastModified;
}
}
UploadingImagesNET8 (Blazor WebAssembly Standalone App)
Simple image loading app ... only accepts png or jpeg
Image file is resized and displayed
<InputFile OnChange="OnChangeHandler" accept="image/png,image/jpeg"/>
@if(imageUrl!=null)
{
<div class="alert alert-secondary">
<p>Old file size: @uploadedFile!.Size.ToString("N0") bytes</p>
<p>New file size: @resizedFile!.Size.ToString("N0") bytes</p>
<img src="@imageUrl"/>
</div>
@*Note: N0 format displays comma seperated numbers NO decimals*@
}
@code {
IBrowserFile? uploadedFile;
IBrowserFile? resizedFile;
string? image;
string? imageUrl;
private async Task OnChangeHandler(InputFileChangeEventArgs e)
{
uploadedFile = e.File;
//Note:
//ContentType is the mime type eg. image/png
//You can also use the parameter 'format' instead
//var format = "image/png";
//... Then the command attempts to convert the current image to the
//specified file type (png ... in this case) with a max dimension of 100x100
resizedFile = await uploadedFile.RequestImageFileAsync(uploadedFile.ContentType, 100, 100);
var buffer = new byte[resizedFile.Size];
var stream = await resizedFile.OpenReadStream().ReadAsync(buffer);
image = Convert.ToBase64String(buffer);
imageUrl = "data:" + uploadedFile.ContentType + ";base64," + image;
}
FileUploadImagesServerNET7 (optional enrichment)
copies downloaded file to the local wwwroot folder
Key ideas
public class Person
{
[Required]
[StringLength(20, MinimumLength = 2)]
public string? Name { get; set; }
[Required]
public IBrowserFile[]? Picture { get; set; }
}
private async Task OnChange(InputFileChangeEventArgs e)
{
//This grabs all files selected by the user using the
//GetMultipleFiles() method and stores them into the
//object property person.Picture
person.Picture = e.GetMultipleFiles().ToArray();
[Inject]
private IWebHostEnvironment? env{ get; set; }
//C# lets you add the @ symbol in front of a string to create a
//verbatim string literal where backslashes are not interpreted
//as escape characters
//This path variable holds the location where the files will be "uploaded" on our computer
//In this case we are storing them in the wwwroot folder in the subfolder uploadedimages
var path = env.WebRootPath + @"\uploadedimages\" + person.Picture[i].Name;
InputRadioGroup and InputRadio components
Preamble ... FormValidationIntro3
Used to show that InputRadio was not available as an EditForm component prior to .NET 5 ... InputCheckbox was available
public class Vehicle
{
public string Name { get; set; }
}
@page "/vehiclespage"
@using BlazorNet5Examples.Models
<div>
<h4> Vehicle Selected - @vehicle.Name </h4>
<EditForm Model="vehicle">
<InputRadioGroup @bind-Value="vehicle.Name">
@foreach (var option in rdOptions)
{
<InputRadio Value="option" /> @option <br />
@*Value ... gets or sets the value of this input*@
}
</InputRadioGroup>
</EditForm>
</div>
@code {
Vehicle vehicle = new Vehicle()
{
Name = "Car" // default value
};
List<string> rdOptions = new List<string> { "Car", "Bus", "Motorcycle" };
}
Supplementary Demos ( ... related to InputRadioGroup)
BlazorNet5ExamplesUpdatedVehicleLookup
Added a lookup method for cost of vehicle picked
<h4> Vehicle Selected - @vehicle.Name </h4>
<h5> Cost - @VehicleLookup(@vehicle.Name)</h5>
private string VehicleLookup(string v)
{
double cost = 0;
string formatted = "";
if (v=="Car")
{
cost = 20000.00;
}
else if (v=="Bus")
{
cost = 105000.00;
}
else if (v=="Motorcycle")
{
cost = 12000.00;
}
formatted = cost.ToString("c");
return formatted;
}
BlazorNet5ExamplesUpdatedSurvey (optional enrichment)
public class BlazorSurvey
{
[Required(ErrorMessage = "Please enter a name.")]
public string Name { get; set; }
[Required(ErrorMessage = "Tell us what you think!")]
[RegularExpression("awesome", ErrorMessage = "...are you sure?")]
public string OpinionAboutBlazor { get; set; }
}
@code {
BlazorSurvey survey = new BlazorSurvey();
string message="";
//Implements a class called Op (located in Models folder)
//... and a List structure
List<Op> NewOpinions { get; set; } = new List<Op>
{
new Op{id="terrible",label="vanilla"},
new Op{id="Ok",label="Ok I guess"},
new Op{id="awesome",label="It's awesome"}
};
void HandleSubmit()
{
message = "Thanks " + survey.Name + " for trying out Blazor!";
}
}
<p>Please take a moment to tell us what you think about Blazor.</p>
<EditForm Model="survey" OnValidSubmit="HandleSubmit">
<DataAnnotationsValidator />
<p>Name: <InputText @bind-Value="survey.Name" /></p>
<p>
Opinion about blazor:
<InputRadioGroup @bind-Value="survey.OpinionAboutBlazor">
@foreach (var opinion in NewOpinions)
{
<div class="form-check">
<InputRadio class="form-check-input" id="@opinion.id" Value="@opinion.id" />
<label class="form-check-label" for="@opinion.id">@opinion.label</label>
</div>
}
</InputRadioGroup>
</p>
<ValidationSummary />
<button type="submit" class="btn btn-primary">Submit</button>
</EditForm>
<p>@message</p>
CarsAPISearch (from lecture 113)/CarAPISearchNET5 (Virtualization)
It improves the perceived performance of component rendering (initial startup display) .
A typical list or table-based component might use a C# foreach loop to render each item in the list or each row in the table, like this:
@foreach (var employee in employees)
{
<tr>
<td>@employee.FirstName</td>
<td>@employee.LastName</td>
<td>@employee.JobTitle</td>
</tr>
}
As the size of the list gets large (companies do grow!) rendering all the table rows this way may take a while, resulting in a noticeable UI delay.
Instead, you can replace the foreach loop with the Virtualize component, which only renders the rows that are currently visible.
<Virtualize Items="employees" ItemSize="40" Context="employee">
<tr>
<td>@employee.FirstName</td>
<td>@employee.LastName</td>
<td>@employee.JobTitle</td>
</tr>
</Virtualize>
The Virtualize component calculates how many items to render based on the height of the container and the size of the rendered items in pixels. You specify how to render each item using the ItemContent template or with child content. If the rendered items end up being slightly off from the specified size, the Virtualize component adjusts the number of items rendered based on the previously rendered output.
The Virtualize component will generate output to your UI for as many objects as it figures are visible. You may see the impact of this in your UI by quickly scrolling down to the bottom of the list generated. You may find the end of the list before all the objects are displayed, but after a slight pause the Virtualize component will render the next batch of missing objects
CarAPISearchNET5
<tbody>
@*@foreach (var car in Cars)*@
<Virtualize Items="Cars" Context="car">
@*{*@
<tr>
<td>@car.Make_ID</td>
<td>@car.Make_Name</td>
<td>@car.Model_ID</td>
<td>@car.Model_Name</td>
</tr>
@*}*@
</Virtualize>
</tbody>
FetchMoreCars.razor (10000 car list for enhanced effect)
public class Morecars
{
//remember Guid is Globally Unique Identifier
public Guid Id { get; set; }
public string Name { get; set; }
public int Cost { get; set; }
}
@code {
private List<Morecars> cars;
protected override async Task OnInitializedAsync()
{
cars = await MakeTenThousandCars();
}
private async Task<List<Morecars>> MakeTenThousandCars()
{
List<Morecars> myCarList = new List<Morecars>();
for (int i = 0; i < 10000; i++)
{
var car = new Morecars()
{
Id = Guid.NewGuid(),
Name = $"Car {i}",
Cost = i * 100
};
myCarList.Add(car);
}
return await Task.FromResult(myCarList);
}
}
@*@foreach (var car in cars)*@
@*OverscanCount parameter specifies how many more items to
render before and after the viewable container.
The default is 3. You may want to tweak this to prevent
excessive rendering when you know there will be a lot of
scrolling. Obviously the higher the number the more elements
you'll render ... so use cautiously ... or you will be
right back where to started from'*@
<Virtualize Items="cars" Context="car" OverscanCount="5">
@*{*@
<tr>
<td>@car.Id</td>
<td>@car.Name</td>
<td>@car.Cost</td>
</tr>
@*}*@
</Virtualize>
</tbody>
Suggested Exercise (CCPizza ... update application from Lecture 10 Webform to WASM .NET 6 +)
PizzaStoreUpdatedLecture10 (Webform version)
CCpizzaBlazorWASMupdated
This version incorporates several EditForms that use the InputRadioGroup/InputRadio components for the Pizza Size and Crust Style and an EditForm using a number of InputCheckbox for the Topping choices
Since we are using EditForms we must have Classes they reference
public class Size
{
public string PizzaSizeName { get; set; }
}
public class Crust
{
public string CrustStyleName { get; set; }
}
public class Toppings
{
public bool PepperoniToppingPicked { get; set; }
public bool OnionToppingPicked { get; set; }
public bool GreenPeppersToppingPicked { get; set; }
public bool RedPeppersToppingPicked { get; set; }
public bool AnchoviesToppingPicked { get; set; }
public bool MushroomsToppingPicked { get; set; }
public bool SausageToppingPicked { get; set; }
public bool OlivesToppingPicked { get; set; }
}
In the code section we make instances of each class, set a default value
and create a list of all possible choices that can be used in our InputRadioGroups
Size size = new Size()
{
PizzaSizeName = "Baby"
};
List<string> rdSizeOptions = new List<string> { "Baby", "Mama", "Papa" };
In the HTML we can display the options
<EditForm Model="@size">
Size ($10/$13/$16)<br/>
<InputRadioGroup @bind-Value="size.PizzaSizeName">
@foreach(var option in rdSizeOptions)
{
<InputRadio Value="option"/> @option <br/>
}
</InputRadioGroup>
Size Selected: @size.PizzaSizeName
</EditForm>
CCpizzaWasmHashSet (optional enrichment)
This version removes the use of Checkboxes for the Topping choices and instead uses an Unordered List which the user clicks on to highlight topping choices and implements a new more detailed Class for the Toppings with the use of a HashSet to store the selected toppings
public class ATopping
{
public string ToppingName { get; set; }
public double ToppingPrice { get; set; }
public string ToppingImage { get; set; }
public string Unicode { get; set; }
}
In the code Section
//This Toppings class was replaced by ATopping below
// Toppings toppings = new Toppings()
// {
// PepperoniToppingPicked = true
// };
//This is the more detailed ATopping class being initialized with all the required values
//note the unicode assignments to emoji images
//Go here for more info https://www.w3schools.com/charsets/ref_utf_symbols.asp
List<ATopping> NewToppingOptions = new List<ATopping>()
{
new ATopping{ToppingName="Pepperoni", ToppingPrice=1.50,ToppingImage="pepperoni.png",Unicode="\u2600"},
new ATopping{ToppingName="Onions", ToppingPrice=.75,ToppingImage="onion.png", Unicode="\u2705"},
new ATopping{ToppingName="Green Peppers", ToppingPrice=.50,ToppingImage="greenpepper.png", Unicode="\u2668"},
new ATopping{ToppingName="Red Peppers", ToppingPrice=.75,ToppingImage="redpepper.png",Unicode="\u2622"},
new ATopping{ToppingName="Anchovies", ToppingPrice=2.0,ToppingImage="anchovie.png",Unicode="\u2b50"},
new ATopping{ToppingName="Mushrooms", ToppingPrice=.5,ToppingImage="mushroom.png",Unicode="\u261d"},
new ATopping{ToppingName="Sausage", ToppingPrice=2.5,ToppingImage="sausage.png",Unicode="\u23f0"},
new ATopping{ToppingName="Olives", ToppingPrice=1.0,ToppingImage="olive.png",Unicode="\u2764"},
};
//A HashSet is an optimized collection of unordered, UNIQUE elements
//that provides fast lookups
// ... similiar to a List
// ... BUT a List maintains the order of the elements as they are insert.
// A HashSet is an unordered collection and does not guarantee any specific sequence
HashSet<string> selectedToppings = new HashSet<string>();
protected override void OnInitialized()
{
//Start with the default of Pepperoni automatically selected
selectedToppings.Add("Pepperoni");
}
private void OnToppingClicked(string top)
{
//Select Logic
//When the user clicks a fruit, we want to pass its name to this method
//This method will take the name and attempt to add it to the selectedFruits HashSet
//... Now remember HashSets do not allow duplicates
//... The HashSet Add method will take an item and attempt to add it
//if the item is already present, the call will return false and if it
//got added it'll return true.
if (!selectedToppings.Add(top))
{
selectedToppings.Remove(top);
}
}
Back to the HTML to show you how this new techique of selecting toppings works.
<ul>
@foreach (var topping in NewToppingOptions)
{
<li @onclick=" ( () => OnToppingClicked(topping.ToppingName))"
class="p-1 mb-1 @(selectedToppings.Contains(topping.ToppingName)? "bg-warning": null)"
style="font-size:15px; cursor:pointer">
@topping.ToppingName @topping.ToppingPrice.ToString("c")
<img src="images/@topping.ToppingImage" width="25" height="25"/>
@topping.Unicode
</li>
}
</ul>
In this Lecture we will
Learn that component libraries are collections of components you can drop into your application when you want a certain bit of UI functionality for your app, things like Date Pickers and Tab container components
Discuss why you would want to use a third party component in favor of creating your own .
A number of unique components (UI controls) not found in Blazor
Gauges, Ratings, GoogleMap etc ...
Component libraries take care of many of the css details
Rapid prototyping
you can drop a grid into your application, complete with paging, sorting, filtering etc. vs spending hours/days building one yourself, so you can focus on your business logic and flow .
Thoroughly tested
Once a company has released a few iterations of a component you’d imagine it will have gone through a hefty amount of testing (both internally and by customers) so it’s pretty low risk to use it in your own application (and you’d expect it to be cross browser compatible which is a significant time saver).
So Why Doesn't Microsoft Provide Its Own Blazor Component Library?
Check out the link in the Resources
Update ! .... .NET 8 ... QuickGrid and FluentUI (See discussion and sample demos at end of Lecture Description)
Learn how to incorporate the Radzen Blazor Components Library within a Blazor WebAssembly app (RadzenBlazorComponent1/BlazorRadzenWasmStandaloneNET8)
Check out the Resources for other options
Syncfusion Community Edition (Free)
Blazorise Community Edition (Free)
DevExpress
Telerik
Radzen Blazor Components is a free and open source set of 90+ native Blazor UI controls.
The components are implemented in C#, it takes full advantage of the Blazor framework
Does not depend on or wrap existing JavaScript frameworks or libraries
Both server-side and client-side (Blazor WebAssembly) Blazor are supported
It has built-in Authentication, authorization, user and role management
Install the library ... Radzen Blazor Components are distributed as the Radzen.Blazor nuget package.
You can add them to your project in one of the following ways
Install the package from Package Manager Console Install-Package Radzen.Blazor -Version 3.8.2
or Add the project from the Visual Nuget Package Manager (install current stable version)
Import the Namespace
add the following in to _Imports.razor
@using Radzen
@using Radzen.Blazor
Add Style and Font References
Open the _Host.cshtml file (server-side Blazor) or wwwroot/index.html (client-side Blazor) and include a theme CSS file by adding these snippets (second version embeds Bootstrap) to the HTML Head Section.
<link rel="stylesheet" href="_content/Radzen.Blazor/css/material-base.css">
older version was ... <link rel="stylesheet" href="_content/Radzen.Blazor/css/default-base.css">
Add Javascript Reference
Open the _Host.cshtml file (server-side Blazor) or wwwroot/index.html (client-side Blazor) and include this snippet in the body of the HTML
script src="_content/Radzen.Blazor/Radzen.Blazor.js"
... and finally to implement Dialog, Notification, ContextMenu and Tooltip components register these services in Progam.cs (may not be necessary in newer versions)
add @using Radzen; to the using section
add the following to the Main Task
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new
Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<DialogService>();
builder.Services.AddScoped<NotificationService>();
builder.Services.AddScoped<TooltipService>();
builder.Services.AddScoped<ContextMenuService>();
await builder.Build().RunAsync();
Update ... newer versions
To Use Dialog, Notification, ContextMenu and Tooltip components
Open the MainLayout.razor file and include:
<RadzenComponents />
Open the Program.cs file and include:
builder.Services.AddRadzenComponents();
Learn how to implement a few of the Radzen Blazor components within our WebAssembly app
Simple Button , Textbox, and TextArea components
Simple data binding
Handling simple events
@page "/radzenpage"
<h3>Radzen Demo Page</h3>
@* Simple Button Components *@
<RadzenButton Text="Hello"></RadzenButton>
<RadzenButton Text="Hello" Icon="account_circle" ButtonStyle="ButtonStyle.Secondary"></RadzenButton>
<RadzenButton Text="Hello" Image="images/blazor.png"></RadzenButton>
<br />
<br />
@*Simple Data Binding*@
<RadzenButton Text=@text />
<br />
<br />
@*Handling Simple Events*@
<RadzenButton Click="@ButtonClicked" Text="Warning" ButtonStyle="ButtonStyle.Warning" />
<br />
<RadzenTextBox @bind-Value=@value MaxLength="100" />
<br/>
<RadzenTextArea @bind-Value=@newvalue />
@code {
private string text = "Blazor using Radzen Components";
private string value = "";
private string newvalue = "";
private void ButtonClicked()
{
newvalue = value;
}
}
Highlight some of the other possible components on the Radzen site (Explore Components) https://blazor.radzen.com/
Data -> Tree + Scheduler
Data Grid
Charts -> Styling
Forms -> Switch + Rating + Slider + Mask
Containers -> Tabs
Gauges -> Radial Gauge
Misc -> Split Button + Menu
Create a simple application to highlight some of the Radzen Blazor components (RadzenBlazorComponent2)
Using RadzenBlazorComponent1 as our starting point , clear out most of the contents of the RadzenPage.razor page.
Next we open up NavMenu.razor in the shared folder and remove all the HTML code block (for the side panel)and replace it with Radzen components below. CodeSnippetsRadzenBlazorComponents2.txt
<RadzenPanelMenu> – This component will create Panel menu
<RadzenPanelMenuItem> – This component is used to create the Menu Item under the Panel Menu and its attributes are,
Text – the menu name
Icon – icon for that menu
Path – component to be opened
Now we create a class called Student.cs which will be referenced within our new UI on the RadzenPage.razor
public class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public string Course { get; set; }
public int Rating { get; set; }
}
Finally we return to the RadzenPage.razor page and create our new UI incorporating the following components (we use the RequiredValidator component demo from https://blazor.radzen.com/requiredvalidator ... as our guide )
Note the use of the Components
RadzenTemplateForm
RadzenFieldset
RadzenLabel
RadzenTextBox
RadzenRequiredValidator
RadzenSlider
RadzenDropDown
RadzenRating
RadzenButton
Code Section
//One important difference between IEnumerable and List(besides one being an interface and the other being a concrete class)
//is that IEnumerable is read-only and List is not.
//So if you need the ability to make permanent changes of any kind to your collection(add & remove), you'll need List.
//If you just need to read, sort and/or filter your collection, IEnumerable is sufficient for that purpose.
IEnumerable<string> multipleValues = new string[] { "A Gentle Introduction to Javascript for Beginners", "A Gentle Introduction to Advanced Excel Techiques and VBA" ,
"A Gentle Introduction to ASP.NET for Beginners","A Gentle Intro to Game Development Using C# and MonoGame","C# Intermediate Programming","C# for Beginners"};
//Alternate version
//List<string> multipleValues = new List <string>{" A Gentle ...."};
Student student = new Student()
{
FirstName = "Charlie",
LastName = "Chiarelli",
Age = 45
};
HTML section
<RadzenTemplateForm TItem="Student" Data=@student Submit=@OnSubmit InvalidSubmit=@OnInvalidSubmit>
<div class="col">
<RadzenTextBox style="display: block" Name="FirstName" @bind-Value=@student.FirstName />
<RadzenRequiredValidator Component="FirstName" Text="First name is required" Popup=true Style="position: absolute" />
</div>
<div class="col">
<RadzenSlider @bind-Value=@student.Age TValue="int" Min="10" Max="100" />
@student.Age
</div>
<RadzenLabel Text="Choose Course to Rate" />
<RadzenDropDown AllowClear="true" TValue="string" Style="width:400px"
Data=@multipleValues
@bind-Value=@student.Course />
<RadzenButton ButtonType="ButtonType.Submit" Text="Submit"></RadzenButton>
Advanced Applications which implement Radzen components ... from future Lectures
RadzenDemoLect145RaptorsDB (Start without Debugging)
RadzenDemoLect158MovieWasmRelDB (Start without Debugging)
Offer you two challenges
First, dig deeper into the Radzen Library and create a simple application that utilizes several of the components that were not covered in this lecture.
RadzenBlazorComponents3DatePicker
<RadzenLabel Text="Basic DatePicker ... Get and Set Value" />
<div class="rz-p-12 rz-text-align-center">
<RadzenDatePicker @bind-Value=@datePicked />
</div>
Date Chosen: @datePicked.Value.ToShortDateString()
@code {
DateTime? datePicked = DateTime.Now;
}
Second, take a look at another third party library, and see if you can implement it into a new Blazor application.
check out Syncfusion and Blazorise which are referenced in the External Resources plus any other libraries you can find.
Latest Info (.NET 8)
QuickGrid is a simple and efficient grid component built by the Blazor team
QuickGrid is officially supported with .NET 8.
QuickGrid isn't intended to replace advanced datagrid components such as those from commercial component vendors. Instead, the purpose is:
To provide a convenient, simple, and flexible datagrid component for Blazor developers with the most common needs
To provide a reference architecture and performance baseline for anyone building Blazor datagrid components. Feel free to build on this, or simply copy code from it.
Check out QuickGridIntroFromLecture192
See Lecture 192 for more Info
Learn that the Blazor team has now developed a more extensive library of Blazor components called FluentUI which rival some of the commercial component libraries.
To make it easier to start a project that uses the Fluent UI Web Components for Blazor out of the box, the Blazor Team has created the Microsoft.FluentUI.AspNetCore.Templates template package.
dotnet new install Microsoft.FluentUI.AspNetCore.Templates::4.9.1
The package contains templates for creating Blazor Web App and Blazor WebAssembly Standalone Apps which mimic the regular Blazor templates. The Fluent UI Blazor components are already fully set up. If you choose to use the sample pages when creating a project, all components have been replaced with Fluent UI counterparts (and a few extra have been added). All Bootstrap styling is removed.
Check out FluentUITemplateBlazorWASMLecture195
See Lecture 195 for more info
In this Lecture we will
Learn how to create our own simple library of reusable Blazor components that can be shared across multiple projects .
Blazor allows us to create such component libraries with the help of a new project template called Razor Class Library
We share not only components but also static contents such as images, stylesheets, etc.
Recall Products2 app from Lecture 95 ... we will be using this idea when we create our own component library
This example highlights that in addition to sending parameters to the component we can send content as well. This is very useful when we have HTML markup that we want to use inside the component. We do this by using a RenderFragment parameter. It is a bad solution trying to send HTML code through the regular parameters to the component because it would be hard to maintain and it is not easily readable.
This example also reviewed how to separate our main razor file (Home) into two separate (but still connected) files by creating a Partial class
Implement the RenderFragment inside the Partial class
[Parameter]
public RenderFragment BrowseContent { get; set; }
Then we modify the Home component file
@page "/home"
<div style="text-align:center">
<h1>
@Title
</h1>
<p>
@*Feel free to browse ...
has been replaced by the newly created property*@
@BrowseContent
</p>
<p>
<img src="images/krispies.jpeg" width="300" height="400" class="img-fluid" />
</p>
</div>
And finally we modify the Index file
@page "/"
<Home Title="Welcome to the Products Page">
@*Here we explicitly specify the name of the
RenderFragment property*@
<BrowseContent>
<b>Feel free to browse</b>
</BrowseContent>
</Home>
Build a simple Blazor component library (MyBlazorLibraryDemo)
Create a new Blazor Server App with the name MyBlazorLibraryDemo
Next ... To add a new components library in your Blazor project, right-click on the Solution and choose Add > New Project… option. Choose Razor Class Library project template from the list of available project templates (type into Search box to find it)
name the project MyComponentsLibrary
delete the default files Component1.razor and ExampleJsInterop.cs
go into the wwwroot folder and remove the image and js files
Now we are ready to create our first shareable component in the Razor class library project
Right-click on the class library project and choose Add > New Item… option. Select Razor Component template and give the component name TableUpdate.razor.
We are going to use code-behind so now create a class with the exact same name with the .cs extension
remember to add public partial ... public partial class TableUpdate<TItem>
//TItem is a generic type for Templated Items
//you will see this TItem associated
//with @typeparam in the component file
public partial class TableUpdate<TItem>
{
[Parameter]
public RenderFragment HeaderTemplate { get; set; }
[Parameter]
public RenderFragment<TItem> RowTemplate { get; set; }
[Parameter]
public RenderFragment FooterTemplate { get; set; }
[Parameter]
public IReadOnlyList<TItem> Items { get; set; }
//basic read-only collection interface which includes a Count property
//and an item indexer. This is well suited for a read-only grid display
}
Every reusable component we create in the Razor class library can also have a stylesheet to define the look and feel of the component.
We want our TableUpdate component to generate tables with the dark blue header, we can define these styles for our component in the TableUpdate.razor.css file.
.thead-blue {
background-color: blue ;
color: white;
}
Now we add our markup in TableUpdate.razor component view file.
@typeparam TItem
@*this directive makes the component generic
it uses the type parameter TItem which can now be used
in the component*@
<table class="table table-striped table-bordered">
<thead class="thead-blue">
<tr>
@HeaderTemplate
</tr>
</thead>
<tbody>
@foreach (var item in Items)
{
<tr>
@RowTemplate(item)
</tr>
}
</tbody>
<tfoot>
<tr>
@FooterTemplate
</tr>
</tfoot>
</table>
Now that we have defined our TableUpdate component in the class library project, it is now time to use this component in our Blazor project. Right-click on the Dependencies node (of the Server Project ) in the Solution Explorer and choose Add Project Reference… option from the context menu. Select the MyComponentsLibrary project and click OK.
If you want to use the TableUpdate component on multiple pages then it is recommended to add the reference of the library in _Imports.razor file as well.
@using MyComponentsLibrary
Now it's time to test our library component, the FetchData.razor pages generates a table of data , so it's perfect for our example.
We will create a new FetchData.razor page called FetchDataUpdated.razor and reference our library component on this new copy.
We replace the HTML table section with
<TableUpdate Items="forecasts" Context="forecast">
<HeaderTemplate>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</HeaderTemplate>
<RowTemplate>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</RowTemplate>
</TableUpdate>
Note that Context is not part of the Component but is used to specify the parameter name for all child expressions referenced in our table display
Test out the final result
Consuming images from the Razor class library
Razor class libraries can expose static assets such as images and these assets can be consumed by the Blazor apps that consume the library. Let’s add an image blazor.png in the wwwroot/images folder of our MyComponentsLibrary project. To use this image inside a Blazor component, add a component with the name Logo.razor in the MyComponentsLibrary project.
Add the blazor_logo.jpg image inside the Logo.razor component using the simple img tag.
<img src="images/blazor.png" />
To use the Logo.razor component in the Blazor app, open the FetchDataUpdated.razor page from the Blazor demo app we created above and directly use the Logo components as shown in the code below.
<h1>Weather forecast UPDATED <Logo /> </h1>
Run the project and you will notice that the image is not rendered as you expected. This is because the relative path of the image images/blazor.png is not accessible from outside the class library project.
To fix the above problem, you need to use a special path syntax given below:
_content/{Razor Class Library Name}/{Path to file}
In the above syntax, the {Razor Class Library Name} is the placeholder for the class library name e.g. MyComponentsLibrary. The {Path to file} is the path to file under wwwroot folder.
Let’s fix our image path using the special syntax describe above
Logo.razor
<img src="_content/MyComponentsLibrary/images/blazor.png" width="50" height="50" />
Consuming Stylesheets from the Razor Class Library
We can also add stylesheets in Razor class libraries and the styles defined in those stylesheets can be used by Blazor apps. Let’s add a stylesheet mystyles.css inside wwwroot/css folder
img {
background-color: aqua;
padding: 5px;
border: 2px solid red;
}
To include the mystyles.css file in our Blazor app, we can use the same special syntax we saw above. Open the _Host.cshtml file available in our Blazor server app and include the mystyles.css file inside the head tag using the following link tag.
link href="~/_content/MyComponentsLibrary/css/mystyles.css" rel="stylesheet"
Run the project and you will notice that the styles related to img tag we defined in the mystyles.css file are applied to all the images of the projects.
Learn how to package our component library in a NuGet file. (MyBlazorLibraryDemoPackaged)
A NuGet package takes the form of a zip file with the extension . nupkg. This makes adding, updating, and removing libraries easy in Visual Studio applications.
Right click the MyComponentsLibrary project and go to properties , click on the package tab and fill in the details necessary to identify your package
Now go back to the project and right click and choose Pack. This will rebuild the project and package it into a NuGet file.
Go into File Explorer and the NuGet file should be located in the MyComponentsLibrary folder and specifically the bin/debug folder ... MyComponentsLibrary.1.0.0.nupkg
Learn how to implement your custom component library NuGet Package in a brand new Blazor project (we will create a simple Server App ... NuGetTest)
First create a folder on your drive where you will store all your NuGet packages ... in my Blazor directory I created a folder called LocalFeed
Copy MyComponentsLibrary.1.0.0.nupkg into this folder
Consuming the Local Feed
In the Tools menu, select Options . ...
Find NuGet Package Manager .
Select Package Sources .
Click the green plus button.
Set Name to something useful (such as Local Feed).
Set Source to the path used above, such as C:\Blazor\LocalFeed .
Click “Update.”
Now you can start up the NuGet Package Manager for Solution, Choose Browse and in the Package source you should see Local Feed. Choose "MyComponentLibrary" and install
... and finally before we can implement our custom component we need to go to _Imports.razor and add
@using MyComponentsLibrary
Now we simply go to the FetchData page and add our updated HTML code as we had previously illustrated including the <Logo> component.
Only thing missing is the style sheet ... go to _Host.cshtml and add
link href="~/_content/MyComponentsLibrary/css/mystyles.css" rel="stylesheet"
Supplementary Demo
ModalDialogExample
Here we reference the MyDialog/Dialog Component which implements
a) A simple Parameter type ... Show
b) Two RenderFragment Parameters ... Title and Body
c) Two EventCallback Parameters ... OnCancel and OnOk
In this Lecture we will
Create a simple Blazor Server application that will simulate an online music store with an album catalogue, a shopping cart, and a checkout feature. This example will serve to review many of the concepts we have covered in Sections 9 and 10 of the course
Highlight the finished product first to give an overview of the intended capabilities of the site.
Start by creating a basic Blazor Server App (.NET 5) (OnlineMusicStore1/OnlineMusicStore1NET6Updated)
Create the Album model (in the Data folder)
public class Album
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Image { get; set; }
public string Price { get; set; }
}
.NET 6
public class Album
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public string? Image { get; set; }
public string? Price { get; set; }
}
Create the AlbumDetail component in the Shared folder
This is a non-routable component which will contain the basic layout to display our album information and implement Parameters. Each album will display a "Buy" button directly beside it.
@code {
[Parameter]
public Album Album { get; set; }
[Parameter]
public bool ShowBuyButton { get; set; }
private void Buy()
{
}
private void Remove()
{
}
}
HTML Section
@using OnlineMusicStore1.Data
<div class="container">
<div class="row align-items-center border-bottom">
<div class="col-sm">
<img src="images/@Album.Image" width="50" height="50" class="mr-3" />
</div>
<div class="col-sm">
<h5 class="mt-0">@Album.Name</h5>
</div>
<div class="col-sm">
<h5 class="mt-0">@Album.Description</h5>
</div>
<div class="col-sm">
<h5 class="mt-0">@Album.Price</h5>
</div>
<div class="col-sm">
@if (ShowBuyButton)
{
<button @onclick="Buy">Buy</button>
}
else
{
<button @onclick="Remove">Remove</button>
}
</div>
</div>
</div>
.NET 6
@if (Album!=null)
{
<div class="container"> ...
Next we pass data to the Child Component
Create the AlbumList page which will be used to display the list of albums ... we will also create the albums there in the code section (not the best way ... we will change this later)
This is a routable page so we need to add the @page directive and @using (pointing to the data folder where our Album class is located)
create a folder called images in the wwwroot folder and copy the album images there
Declare the Album list in the code section
@code {
private List<Album>? Albums;
//The OnIntialized() method fires when the page is rendered on the server
//By doing this you are telling the server to initiate the value for the
//Albums after the page is rendered.
protected override void OnInitialized()
{
Albums = new()
{
new()
{
Id = Guid.NewGuid(),
Name = "Thriller",
Description = "Michael Jackson",
Image="jackson-thriller.jpg",
Price="10.00"
},
new()
{
Id = Guid.NewGuid(),
Name = "Dark Side of the Moon",
Description = "Pink Floyd",
Image="pinkfloyd-darksideofmoon",
Price="10.00"
},
new()
{
Id = Guid.NewGuid(),
Name = "Rumors ",
Description = "Fleetwood Mac",
Image="fleetwoodmac-rumors.jpg",
Price="10.00"
}
};
}
}
Alternate method *** Preferred ***
Albums = new List<Album>()
{
new Album(){Id = Guid.NewGuid(),Name = "Thriller",Description = "Michael Jackson",Image = "jackson-thriller.jpg",Price = "10.00"},
new Album(){Id = Guid.NewGuid(),Name = "Dark Side of the Moon",Description = "Pink Floyd",Image = "pinkfloyd-darksideofmoon.jpg",Price = "10.00"},
new Album(){Id = Guid.NewGuid(),Name = "Rumors ",Description = "Fleetwood Mac",Image = "fleetwoodmac-rumors.jpg",Price = "10.00"}
};
Add the code to the HTML section to display the details about each Album
@page "/albumlist"
@using OnlineMusicStoreNET6.Data
Simulating a online music website with a Album catalogue, a shopping cart and a checkout feature
@*.NET 6 very particular about checking for null*@
@if (Albums==null)
{
<p><em>Loading ... </em></p>
}
else
{
<div class="container">
<h3>Greatest Albums Of All Time</h3>
<hr />
<div class="row align-items-center border-bottom">
<div class="col-sm">
<h5 class="mt-0">Album Cover</h5>
</div>
<div class="col-sm">
<h5 class="mt-0">Album Name</h5>
</div>
<div class="col-sm">
<h5 class="mt-0">Description</h5>
</div>
<div class="col-sm">
<h5 class="mt-0">Price</h5>
</div>
<div class="col-sm">
<h5 class="mt-0">Buy</h5>
</div>
</div>
@foreach (var album in Albums)
{
<AlbumDetail Album="album" ShowBuyButton="true"></AlbumDetail>
}
Here we are passing 2 parameters to the AlbumDetail Component the album name and the boolean value true which will indicate to display BUY beside the album info ... as opposed to REMOVE (false) which we will implement later in our checkout page
add a link in the NavMenu to navigate to the Album List
add a navigation link to the Checkout page (we will make this page later)
<NavLink href="checkout">Check Out</NavLink>
Test out the first version of our Online Music Store
In this Lecture we will
Continue our work on the Online Music Store (OnlineMusicStore2/OnlineMusicStore2NET6Updated)
At this stage of development the online store has an album list and the user can navigate to the Album List and from the Album List to the Check-Out page (soon to be created)
Learn about the concept of a Service.
A service is an instance of a class that you can make available anywhere on your website using Dependency Injection
Currently, the Album list is only on the AlbumList page and if the Checkout Page needs to display some information about the Album you must re-type it there
So our next step is to build a way for the website to share the Album List so that both pages and more can access the information.
make note of WeatherForecast.cs and WeatherForecastService.cs
Also recall Lecture 109-111 SongList Database where we created a SongService.cs (BlazorSongLIst4UpdateLect111)
Now create a new class called AlbumService (in the data folder) where we will create all our albums (it serves as a data provider) ... use AlbumService.txt code snippet
private readonly List<Album> _albums;
public AlbumService()
{
_albums = new()
{
new()
{
Id = Guid.NewGuid(),
Name = "Thriller",
Description = "Classic Michael Jackson",
Image = "jackson-thriller.jpg",
Price = "10.00"
},
new()
{
Id = Guid.NewGuid(),
Name = "Dark Side of the Moon",
Description = "Pink Floyds best",
Image = "pinkfloyd-darksideofmoon.jpg",
Price = "12.70"
},
new()
{
Id = Guid.NewGuid(),
Name = "Rumors ",
Description = "Fleetwood Mac",
Image = "fleetwoodmac-rumors.jpg",
Price = "9.99"
}
};
}
//Preferred Method
//_albums = new List<Album>()
//{
// new Album(){Id = Guid.NewGuid(),Name = "Thriller",Description = "Michael Jackson",Image = "jackson-thriller.jpg",Price = "10.00"},
// new Album(){Id = Guid.NewGuid(),Name = "Dark Side of the Moon",Description = "Pink Floyd",Image = "pinkfloyd-darksideofmoon.jpg",Price = "10.00"},
// new Album(){Id = Guid.NewGuid(),Name = "Rumors ",Description = "Fleetwood Mac",Image = "fleetwoodmac-rumors.jpg",Price = "10.00"}
//};
public List<Album> GetAllAlbums()
{
return _albums;
}
}
Next we will Register this new service in Startup.cs in the ConfigureServices section
services.AddSingleton<AlbumService>();
service lifetime is defined as Singleton which means a single instance of the class will be created and that instance will be shared throughout the application. Any components that use the service will receive an instance of the same service.
.NET 6 or higher no Startup.cs .... update Program.cs
builder.Services.AddSingleton<AlbumService>();
Finally we go to the AlbumList page and inject the AlbumService
@inject AlbumService AlbumService
Remove most of the code from OnInitialized and add
Albums = AlbumService.GetAllAlbums();
Test out again ... there should be no change in the output
Create the cart service
Then next step is to build a way for users to add albums they want to purchase to a cart via a Buy button. Here we set up a cart service to store information about albums in the cart.
Note: The readonly modifier ensures the field can only be given a value during its initialization or in its class constructor. You can't inadvertently change it from another part of that class after it is initialized.
First we create a new class in the Data folder called CartService.cs
public class CartService
{
private readonly List<Album> _items;
public CartService()
{
_items = new();
}
public void AddToCart(Album item)
{
_items.Add(item);
}
public List<Album> GetAlbumsInCart()
{
return _items;
}
}
Then we register the service in Startup.cs
services.AddScoped<CartService>();
CartService will serve data differently for each customer so you will register the CartService as a scoped service.
A Scoped service creates a new instance for each request
.NET 6
builder.Services.AddScoped<CartService>();
Now we inject this service into the AlbumDetail component and update the Buy() method
private void Buy()
{
if (Album!=null)
{
CartService.AddToCart(Album);
}
}
Create the checkout view
For customers to checkout, they need to see their cart
Create a razor component page with routing called Checkout.razor
Check if the link to the Checkout page works (no page contents yet)
Now we need to display the items in the cart on this page
import the CartService and make a reference to the Data folder
@using OnlineMusicStore1.Data
@inject CartService CartService
Load in the list of albums that are in the cart
private List<Album> AlbumsInCart = new();
protected override void OnInitialized()
{
AlbumsInCart = CartService.GetAlbumsInCart();
}
update the HTML to display the items.
<div class="row bg-success text-white" style="margin-top:10px; height:40px">
<label class="p-2">CHECKOUT</label>
</div>
Products in cart:
<div class="d-flex flex-column">
@foreach (var album in AlbumsInCart)
{
<AlbumDetail Album="album" ShowBuyButton="false"></AlbumDetail>
}
<br />
</div>
<br />
Notice that the second parameter ShowBuyButton is set to false so now you should see the REMOVE button displayed in the cart (we will code this action in the next lecture)
Test out the current version of our Online Music Store
In this Lecture we will
Complete the Online Music Website (OnlineMusicStore3/OnlineMusicStore3NET6)
Implement a Form based checkout feature to collect user information as part of the checkout
recall the <EditForm> tag
First we need to create the form model (class) which we will call CheckOutInfo.cs incorporating Data Annotations
public class CheckoutInfo
{
[Required(ErrorMessage = "Name Required")]
public string Name { get; set; }
[Required(ErrorMessage = "Address Required")]
public string Address { get; set; }
}
Now go back to the Checkout page and add the pre-built EditForm with validations and the code to submit the form in the code section
First in the code section we add
private CheckoutInfo CheckoutInfo = new();
which we will reference in our HTML EditForm
Then we create a HandleValidSubmit method which is required by our EditForm
private void HandleValidSubmit()
{
// Here we passed the string Thank you as a parameter to the JavaScript alert function
JsRuntime.InvokeVoidAsync("alert", $"Thank you {CheckoutInfo.Name}, delivery to {CheckoutInfo.Address}");
}
Don't forget we need to inject IJSRuntime to access the Javascript calls
@inject IJSRuntime JsRuntime
Now we add our HTML EditForm which references via the Model parameter the instance of our class CheckoutInfo ... this is the first method... later we modify our EditForm to use EditContext (second method)
Customer Info:
<EditForm Model="CheckoutInfo" OnValidSubmit="HandleValidSubmit" >
<DataAnnotationsValidator />
<div>
<label class="col-form-label" for="name">Name:</label>
<InputText id="name" class="form-control" @bind-Value="CheckoutInfo.Name"></InputText>
<ValidationMessage class="form-control" For="()=>CheckoutInfo.Name"></ValidationMessage>
</div>
<div>
<label class="col-form-label" for="address">Address:</label>
<InputText id="address" class="form-control" @bind-Value="CheckoutInfo.Address"></InputText>
<ValidationMessage class="form-control" For="()=>CheckoutInfo.Address"></ValidationMessage>
</div>
<div>
<button class="btn btn-primary" type="submit">Submit</button>
</div>
</EditForm>
Finally we will add a couple of extra features/enhancements to our Checkout page
add Return to Albums Page link at the bottom of the form below the Submit Button
<NavLink href="albumlist">Return to Albums Page</NavLink>
add Clear Customer Info logic
First add add to bottom of form
<div>
<button class="btn btn-primary" type="submit">Submit</button>
<button type="reset" class="btn btn-secondary">Clear Customer Info</button>
</div>
Next modify EditForm statement to include @onreset
<EditForm Model="CheckoutInfo" OnValidSubmit="HandleValidSubmit" @onreset="HandleReset">
Finally add the code to handle the reset
private void HandleReset()
{
CheckoutInfo = new();
}
add Total bill logic
First we add a little bit of code
double Total;
string Bill;
protected override void OnInitialized()
{
AlbumsInCart = CartService.GetAlbumsInCart();
Total = 0;
foreach (var album in AlbumsInCart)
{
Total += double.Parse(album.Price);
}
Bill = Total.ToString("c");
}
Next we add the HTML code underneath the foreach loop
<label class="p-2">Total: @Bill</label>
add Remove button logic (most of the logic occurs in other files)
First we need to go into the CartService class and add
public void RemoveFromCart(Album item)
{
_items.Remove(item);
}
Next we go into AlbumDetails where we have made a reference to Remove
In the code section we add
private void Remove()
{
CartService.RemoveFromCart(Album);
NavigationManager.NavigateTo("/albumlist");
//The NavigateTo moves you back to the front page
//and in essence forces a refresh, so that when you go back
//to the Cart the removed album is no longer there
}
We also need to inject the NavigationManager
@inject NavigationManager NavigationManager
Remember ... using .NET 6+
if (Album!=null)
{
CartService.RemoveFromCart(Album);
}
... As an alternative to Navigating back to the AlbumList page to refresh/re-render the Checkout page we can add a button on the Checkout page ... with the title Update Cart (event handlers automatically trigger UI re-renders)
with the associated method
private void UpdateCart()
{
Total = 0;
foreach (var album in AlbumsInCart)
{
Total += double.Parse(album.Price);
}
Bill = Total.ToString("c");
}
... now we can update our OnInitialized method to just call the UpdateCart method
add a more detailed display of Albums purchased and Customer info (... in Checkout.razor)
To access the Customer Info we will need to implement a reference to EditContext in our EditForm
First we make a reference to EditContext in our code section
private EditContext? editContext;
... then make an instance of it in our OnInitialized method
editContext = new EditContext(CheckoutInfo);
We also update our HandleReset
editContext = new EditContext(CheckoutInfo);
Now we update our HTML EditForm to reference EditContext
<EditForm EditContext="@editContext" OnValidSubmit="HandleValidSubmit" @onreset="HandleReset">
Finally to gain access to all the details we will need to access a bit of Javascript
First we will add a new reference so that we can implement the JsonSerializer
@using System.Text.Json
Next we create a js folder in wwwroot and create a javascript file called myscript with the displayAlert function
function displayAlert(modelJ) {
alert("Album Purchase Details " + modelJ);
}
Before we can use this file we need to reference it in _Host.cshtml
and finally in the HandleValidSubmit method we remove the original simple javascript call and add
var modelJson1 = JsonSerializer.Serialize(CheckoutInfo, new JsonSerializerOptions { WriteIndented = true });
JsRuntime.InvokeVoidAsync("displayAlert", modelJson1);
var modelJson2 = JsonSerializer.Serialize(AlbumsInCart, new JsonSerializerOptions { WriteIndented = true });
JsRuntime.InvokeVoidAsync("displayAlert", modelJson2);
JsRuntime.InvokeVoidAsync("alert", $"Final Amount Due: {Bill}");
//... or
// JsRuntime.InvokeVoidAsync("alert", "Final Amount Due: " + Bill.ToString());
... and a call to UpdateCart ... just in case
Supplementary Demo (SummativeExtraUpdated)
This application serves as a simple ... short ... review of some of the basic concepts covered in Lectures 135-137
We will create a simple card wall to display top rated movies with a short summary and image
We work through 3 versions from most simplistic to implementing parameters to implementing classes (services) and dependency injection
First we create a Razor Component called Movies.razor
In this first version we basically use hard coded HTML and the Bootstrap card component.
<div class="col">
<div class="card h-100">
<div class="card-body">
<h5>ET</h5>
<p>The Steven Spielberg Classic</p>
<img src="images/ET1982.jpg" height="100" width="90" />
</div>
</div>
</div>
In version two we realize the fact we are now duplicating basically the same exact markup for each movie so we introduce a Shared component Definition ... in the Shared folder ... which implements Parameters ... now we can remove a lot of the clutter from the Movie page
<div class="col">
<div class="card h-100">
<div class="card-body">
<h5>@Name</h5>
<p>@Summary</p>
<img src="images/@Image" height="100" width="90" />
</div>
</div>
</div>
@code {
[Parameter]
public string Name { get; set; }
[Parameter]
public string Summary{ get; set; }
[Parameter]
public string Image{ get; set; }
}
<Definition Name="ET"
Summary="The Steven Spielberg Classic"
Image="ET1982.jpg"/>
Now that we have got a handle on the application we are going to get rid of all the repeated markup by pushing the Movie details into a C# collection
First we create a class in the Data folder called ProductListItem (actual class will be called DefinitionItem) ... consisting of Name,Summary and Image
public class DefinitionItem
{
public string? Name { get; set; }
public string? Summary { get; set; }
public string? Image { get; set; }
}
Now we will create a Service (class) called DefinitionStore which will be used to Fetch our data ... this could be replaced eventually by database calls.
public class DefinitionStore
{
List<DefinitionItem> definitions = new List<DefinitionItem>()
{
new DefinitionItem(){Name="ET",Summary="The Steven Spielberg Classic",Image="ET1982.jpg"},
new DefinitionItem(){Name="The GodFather Part 2",Summary="Academy Award Winner for Best Picture",Image="TheGodFather1972.jpg"},
new DefinitionItem(){Name="Raging Bull",Summary="Classic Robert DeNiro performance",Image="RagingBull1980.jpg"},
new DefinitionItem(){Name="Psycho",Summary="A Hitchcock Thriller",Image="Psycho1960.jpg"}
};
public List<DefinitionItem>GetDefinitions()
{
return definitions;
}
}
Next we register this Service in Program.cs
builder.Services.AddScoped<DefinitionStore>();
... And finally we are ready to implement this service on our Movies page
We load in our data via an Oninitialized() method and then implement a foreach loop to cycle through all the movies calling our shared component 'Definition' with its 3 parameters
@page "/movies"
@using BlazorAppPre8.Data
@inject DefinitionStore Store
@code {
private List<DefinitionItem> definitions;
protected override void OnInitialized()
{
definitions = Store.GetDefinitions();
}
}
<div class="row row-cols-1 row-cols-md-3 g-4">
//depending on screen size 1-col up to 3 col
@foreach (var d in definitions)
{
<Definition Name="@d.Name"
Summary="@d.Summary"
Image="@d.Image" />
}
</div>
In this Lecture we will
Recreate the Online Music Store application as a Blazor WebAssembly application .
Implement a JSON file to store the Album information
Start by creating a basic Blazor WebAssembly App (.NET 5) (OnlineMusicStoreWASM1)
First add a folder called images in the wwwroot folder and copy over the album images
Now Update the Index.razor page with a new opening screen
@page "/"
<h1>Welcome To The Chiarelli Music Store</h1>
<img src="images/jackson-thriller.jpg" width="100" height="100"/>
<p class="lead">
"The all time best albums"
</p>
Add a new folder called Models and create the Album class
public class Album
{
public string Name { get; set; }
public string Description { get; set; }
public string Image { get; set; }
public string Price { get; set; }
}
Go into the wwwroot/sample-data folder and create a JSON file called albumlist.json
Recall Lecture 112
JSON stands for JavaScript Object Notation.
JSON objects are used for transferring data between server and client
JSON is a text based data exchange format that uses key:value pairs.
JSON style:
{"students":[
{"name":"John", "age":"23", "city":"Toronto"},
{"name":"Steve", "age":"28", "city":"New York"},
{"name":"Peter", "age":"32", "city":"Paris"},
{"name":"Charlie", "age":"28", "city":"Rome"}
]}
Add the following information into the albumlist.json
[
{
"Name": "Thriller",
"Description":"Classic Michael Jackson",
"Image":"jackson-thriller.jpg",
"Price":"10.00"
},
{
"Name": "Dark Side of the Moon",
"Description": "Pink Floyds best",
"Image": "pinkfloyd-darksideofmoon.jpg",
"Price": "12.70"
}
]
Now lets focus on our main page which we will call Store.razor in the Pages folder.
Before entering all the HTML and C# code , create a link in the NavMenu.razor file to the Store.razor page
Notice in the Store.razor page the injection of HttpClient which is necessary to access the JSON file.
@page "/store"
@using OnlineMusicStoreWASM.Models
@inject HttpClient Http
HTML markup (see coding snippet in resources ... StorePageHTML.txt for full html)
Notes:
@* Recall that when you need to use Arguments
with a method you use Lamda expression*@
<td class="align-middle">
<button class="btn btn-primary"
@onclick="@(() =>
AddAlbum(item))">
Add to Cart
</button>
</td>
@if (cart.Any())
{
<h2>Your Cart</h2>
<ul class="list-group">
@foreach (Album item in cart)
{
<li class="list-group-item p-2">
<button class="btn btn-sm"
@onclick="@(() =>DeleteAlbum(item))">
<span class="oi oi-delete">
</span>
</button>
@item.Name - $@item.Price
</li>
}
</ul>
<div class="p-2">
<h3>Total: @total.ToString("c")</h3>
</div>
}
@code {
//Here we use the interface List (IList) to allow us access to the JSON file via the Http.GetFromJsonAsync command
//An Interface List is similiar to a regular List They both represents a collection of objects accessed by index
//A List is a concrete class and IList is an interface.
//A List implements the IList interface
public IList<Album> albums;
public IList<Album> cart = new List<Album>();
private double total;
protected override async Task OnInitializedAsync()
{
albums = await Http.GetFromJsonAsync<List<Album>>("sample-data/albums.json");
}
private void AddAlbum(Album album)
{
cart.Add(album);
total += double.Parse(album.Price);
}
private void DeleteAlbum(Album album)
{
cart.Remove(album);
total -= double.Parse(album.Price);
}
}
Test the application ... notice that when we navigate between the pages the state is lost ie. we lose track of our purchases
This is where the need to implement a service comes and the use of Dependency Injection.
Supplementary Demo
OnlineMusicStoreWASM1Breadcrumbs
Index.razor
<div class="bg-light">
<ol class="breadcrumb">
<li class="breadcrumb-item active">Home ... Note the use of Bread Crumbs</li>
</ol>
</div>
Store.razor
<ol class="breadcrumb bg-light">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item active">Store</li>
</ol>
In this Lecture we will
Finish working on our new version of the Online Music Store recreated in Blazor WebAssembly (OnlineMusicStoreWASM2)
Learn about the use of an C# Interface that will be inherited by the CartService class.
Learn about the use of events, and delegates
Learn about the publisher of an event and the subscriber to an event
Learn about raising an event
Learn about using the StateHasChanged method handler
First will we create a folder called Services
Now we create an interface called ICartService
Think of an interface as a class with the implementation stripped out. An Interface doesn't actually do anything. It merely defines what a class that implements(inherits) it will do. It indicates what sorts of methods, properties and events are exposed by an object
Why Use Interfaces ?
Makes your code base more scalable and makes code reuse much more accessible because the implementation is separated from the interface
interface ICartService
{
IList<Album> Cart { get; set; }
double Total { get; set; }
//An event is a notification sent by an object to signal the occurrence of an action.
//In C# an event is an encapsulated delegate. It is dependent on the delegate
//The delegate defines the signature for the event handler method
//So to declare an event ...
// 1) declare a delegate
// 2) declare a variable of the delegate with the event keyword
//In the code below we declare a delegate of type Action ... specifies the signature where ...
// Action is a delegate type in C# that encapsulates a method that does not return a value
// and then declare an event called OnChange of delegate type Action
// OnChange is called the publisher
// The subscriber will be located in the MainLayout.razor page
event Action OnChange;
void AddAlbum(Album album)
{
}
void DeleteAlbum(Album album)
{
}
}
Next we create the CartService which inherits the ICartService
//Here the CartService INHERITS from the Interface . After adding : ICartService ... implement interface by choosing 'fix'
public class CartService : ICartService
{
public IList<Album> Cart { get; set; }
public double Total { get; set; }
//Constructor ... this removes the need to make an instance of Cart
//in the Store.razor code section ... we will use Dependency Injection
public CartService()
{
Cart = new List<Album>();
}
//Raising an Event
//The code below is necessary to have our app update everywhere (Total in this case)
//when we modify our Album picks/removals ... automatically re-render
//This will give us something we can react to when the CartService gets updated
//We will subscribe to this event (OnChange) in the MainLayout.razor page
public event Action OnChange;
//The OnChange event is invoked when the NotifyStateChanged method is called
//note the use of the null-conditional operator ? returns null if the left hand operand is null
private void NotifyStateChanged()
{
OnChange?.Invoke();
}
public void AddAlbum(Album album)
{
Cart.Add(album);
Total += double.Parse(album.Price);
NotifyStateChanged();
}
public void DeleteAlbum(Album album)
{
Cart.Remove(album);
Total -= double.Parse(album.Price);
NotifyStateChanged();
}
}
After completing the CartService we need to register the service in the Program.cs file ... after which you can inject it into any page
builder.Services.AddScoped<ICartService, CartService>();
Now we can Inject the CartService in the Store.razor page
@using OnlineMusicStoreWASM.Services
@inject ICartService cartService
@* Note changes to AddAlbum ,DeleteAlbum , Total and cart reference (now called Cart) ... now that we are accessing the injected cartService*@
Note also the updated @code section ... we only need
public IList<Album> albums;
protected override async Task OnInitializedAsync()
... the cart declaration and total are now taken care of by the CartService
... And finally let's add the cart total to all of the pages
Updating the Shared\MainLayout.razor page will allow the cart total to appear everywhere
add using and injections to top of page
@using OnlineMusicStoreWASM.Services
@inject ICartService cartService
add markup
<div class="top-row px-4">
<h3>Cart Total: @cartService.Total.ToString("c")</h3>
<a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
</div>
Time to test it out ... remember the whole point of this Service implementation was to Maintain State. So if we move away from the Store page and then return the current Cart contents should still be there
... But after testing we do still have one issue. Note how the cart total at the top of the page is not being updated as we add new items to the cart. We need to deal with this.
Recall Lecture 137 where we used an "Update Cart" button to re-render the Checkout Page after we removed an Album ... it worked but there is a more formal technique that senses when a change has occurred automatically without the need to manually click update.
We need to notify the component when it needs to be updated. We will need to call the StateHasChanged method whenever the OnChange method of the CartService is invoked. We do this as follows
First add @implements IDisposable to top of page with other directives... This is not necessary to have the fix work but is good housekeeping so we manage our resources properly.
Next go down to the code section and add
@code {
//Here we call the StateHasChanged method (Handler) whenever the
//OnChange method of CartService is invoked This will cause the component to be re-rendered
//That is, everytime the Cart is updated the OnChange event will be fired
//and the MainLayout.razor will be re-rendered to show the latest data
protected override void OnInitialized()
{
//Here we are subscribing (registering += operator) to the event that is we are receiving notification of an event
cartService.OnChange += StateHasChanged;
}
//You must unsubscribe from the event to prevent the StateHasChanged
//method from being invoked each time the cartService.OnChange
//event is raised. Otherwise your application will experience resource leaks
public void Dispose()
{
//Here we are unsubscribing (-= operator) to prevent event propogation and memory leaks
cartService.OnChange -= StateHasChanged;
}
}
Test out the final version of the Online Music Store ... WebAssembly version
Supplementary Demos and Suggested Exercises
RaisingAnEventReview
Nice review of the concept of Events covered in this Lecture. We have two components called RedComponent and BlueComponent which contains buttons which when clicked will change the colour of Text located on another Component called RaiseEventPage
First we create a Service/Class called AppState
public string? SelectedColour { get; set; }
public event Action OnChange;
public void SetColour(string colour)
{
SelectedColour = colour;
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
Our BlueComponent and RedComponent are simple non-routable components
@using RaisingAnEventReview.Models
@inject AppState AppState
<h5>Blue Component</h5>
<button class="btn btn-primary" @onclick="SelectColour">Select Blue</button>
@code {
private void SelectColour()
{
AppState.SetColour("Blue");
}
}
... and finally here is the RaiseEventPage.razor
@page "/raiseeventpage"
@using RaisingAnEventReview.Models
@inject AppState AppState
@implements IDisposable
<h3 style="color:@AppState.SelectedColour">Raise Event Page ... @AppState.SelectedColour</h3>
<hr/>
<RedComponent/>
<BlueComponent/>
@code {
//This last component ties everything together.
//It handles the OnChange event exposed by the AppState class.
//Whenever the selected colour is changed StateHasChanged will be
//invoked and the component will re-render with the new selected colour
protected override void OnInitialized()
{
AppState.OnChange += StateHasChanged;
}
//It's important to remember to unsubscribe the components StateHasChanged
//method from the AppState's OnChange event - otherwise we could introduce
//a memory leak. We do this my implmenting the IDisposable interface
public void Dispose()
{
AppState.OnChange -= StateHasChanged;
}
}
BookAppServiceCartEventCallbackLect139
This is a simple book store application. You can add books to the cart by clicking the Add to Cart button... and Remove items from the Cart once they have been added.
This is a basic review of using Services and EventCallback
SummativeExercisePizzaStore
Simple problem that mirrors the application we just finished ... I offer two solutions, one without a Service and one with a Service.
PizzaStoreApp1WithoutService
Note the use of the constructor in the Pizza.cs class
public class Pizza
{
public string Name { get; set; }
public string Price { get; set; }
public string Spiciness { get; set; }
public Pizza( string name, string price, string spiciness)
{
this.Name = name;
this.Price = price;
this.Spiciness = spiciness;
}
}
In the code section we fill up the pizzas List using the constructor
protected override void OnInitialized()
{
pizzas = new List<Pizza>()
{
new Pizza("Pepperoni", "8.99", "Spicy" ),
new Pizza("Margarita", "10.99", "None" ),
new Pizza("Diabolo", "9.99", "Hot" ),
new Pizza("Hawaiian","12.99","None")
};
}
.... as opposed to
pizzas = new List<Pizza>()
{
new Pizza() { Name="Pepperoni",Price="8.99", Spiciness="Spicy"},
};
Note use of method using lambda expression for image
<td>
<img src="@SpicinessImage(pizza.Spiciness)" width="50" height="50" />
@pizza.Spiciness
</td>
private string SpicinessImage(string spiciness)
=> "images/"+ spiciness.ToString().ToLower()+ ".png";
PizzaStoreApp2WithService
MoreBlazorIntroExercises
A set of 3 practical applications (MoreBlazorPracticeProblems.pdf)
Toronto Raptors Roster Page (TeamMembers) ... this is a redo of a similar problem from Lecture 9) Choose a player from a DropDown list and display player image and background info. This exercise forces you to use a JSON file to store the player information.
<select class="form-select" @bind="@Img">
<option value="@Img">Pick Player from List</option>
@foreach (var item in players)
{
<option value="@item.Image">@item.PlayerName</option>
}
</select>
Note how the background info is displayed based on value of Img (string)
<div class="card-header">
<span class="alert alert-danger">Details: @(PlayerLookup(@Img))</span>
</div>
//Since we are binding based on the Image
//I used a simple lookup to determine the other player info
private string PlayerLookup(string img)
{
string details = "";
foreach (var item in players)
{
if (img == item.Image)
{
details = item.PlayerName + " - " + "Position " + item.Position;
}
}
return details;
}
Casino Slots (Casino) ... Check out the solutions (several versions ... this is a redo of a similar problem from Lecture 16) with the most advanced implementing a Timer for animation and Javascript for sound effects
CasinoSlotsWebFormLecture16 (orginal WebForm version ... a starting point)
CasinoSlotsBlazorWASM (static display no animation)
Note the use of a string array to store the image names
string[] images = new string[] { "cherry", "shamrock", "horseshoe" };
//this creates images[0]="cherry"
// images[1]="shamrock"
// images[2]="horseshoe"
private void PullLeverAndPlay(int b)
{
//pick numbers between 0 and 2
int slot1 = r.Next(0, 3);
int slot2 = r.Next(0, 3);
int slot3 = r.Next(0, 3);
//display the actual matching images on the page
Image1 = "images/" + images[slot1] + ".jpg";
Image2 = "images/" + images[slot2] + ".jpg";
Image3 = "images/" + images[slot3] + ".jpg";
DetermineWinnings(slot1, slot2, slot3, b); //pass along the random numbers + AND the bet b to the method
}
CasinoSlotsBlazorWASMwithTimer
This application serves as an Extension of the Casino Game covered in a previous example
First check out the Timer Demo Page ... this will introduce you to the main concept necessary for simulating simple animation
Note the use of ... PeriodicTimer timer declaration
Note the use of ... timer = new PeriodicTimer(TimeSpan.FromSeconds(1))
Note the user of ... while (await timer.WaitForNextTickAsync())
See all the comments for detailed explanations
// To simulate a count down we need some kind of tick event every second
// To do this we will employ a timer based on the PeriodicTimer class
PeriodicTimer? timer;
private string remaining => TimeSpan.FromSeconds(timeleft).ToString(@"mm\:ss");
protected async Task Start()
{
timer?.Dispose(); //This handles a subtle issue when a user presses Start
//a few times in a row without clicking the stop button
//...so we always start fresh and kill the timer
//to start off
timeleft = 2 * 60; //2 minutes ... but in seconds 120
//remaining expression above will display it in min and sec
flag = true; //used to enable and disable the Start buton
col = "green";
//tick every 1 second ... ie wait a second and then change something ... in our case
//the value of timeleft (our count down)
timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
//we invoke WaitForNextTickAsync where we put our business logic
//ie what we want to change every tick ... an infinite loop
//until we decide to stop by pressing the Stop button in the UI
//or we get down to 0
while (await timer.WaitForNextTickAsync())
{
if (timeleft>0)
{
timeleft -= 1;
StateHasChanged();//need this to refresh (re-render) the screen and see updated timeleft
}
else
{
col = "red";
Stop();
}
}
}
With this knowledge we then modify the Casino game to implement these new commands that are necessary to simulate each slot spinning around a set number of times before stopping at the final result
private int currentspin;//used to track the number of times we spin the slot machine
protected async Task PullLeverAndPlay(int b)
{
timer?.Dispose();
//change the images every 1 second ... this is arbitrary ... I changed it to .2
timer = new PeriodicTimer(TimeSpan.FromSeconds(.2));
//we invoke WaitForNextTickAsync where we put our business logic ...
//randomly pick images for the slots
//display them for 1 sec and then pick a new round of images
while (await timer.WaitForNextTickAsync())
{
currentspin++;
//pick numbers between 0 and 2
slot1 = r.Next(0, 3);
slot2 = r.Next(0, 3);
slot3 = r.Next(0, 3);
//display the actual matching images on the page
Image1 = "images/" + images[slot1] + ".jpg";
Image2 = "images/" + images[slot2] + ".jpg";
Image3 = "images/" + images[slot3] + ".jpg";
StateHasChanged();
if (currentspin > 9) //stop after 10 spins ... this is arbitrary ... make it whatever you want I changed it to 10
{
DetermineWinnings(slot1, slot2, slot3, b); //pass along the random numbers + AND the bet b to the method
StateHasChanged();
Stop();
}
}
}
CasinoSlotsBlazorWASMwithTimerAudio
Start without Debugging if issues Javscript PlayAudioFile script
Note the inclusion of the HTML audio tag
<audio id="player">
<source id="playerSource" src="" />
</audio>
protected async Task PlaySound()
{
//calling Javascript function PlayAudioFile
//Javscript is located in wwwroot folder under js subfolder
//Actual script is called myscript and it contains the function called PlayAudioFile
//... also located in the wwwroot folder is a sound folder containing the mp3 file(s)
await JsRuntime.InvokeVoidAsync("PlayAudioFile", "/sounds/slotmachinepull.mp3");
}
Javascript
//This code finds the audio player, sets the source file
//loads it and plays it
function PlayAudioFile(src) {
var audio = document.getElementById('player');
if (audio != null) {
var audiosource = document.getElementById('playerSource');
if (audiosource != null) {
audiosource.src = src;
audio.load();
audio.play();
}
}
}
CasinoSlotsBlazorWASMTimerAudioGIF
Animated GIF plays if you get 3 Horeshoes or 3 Cherries
@if(Result=="Three Horseshoes!!!" || Result=="Three Cherries")
{
<img src="images/loading.gif" width="100" height="100" />
}
CCPizza Problem (Pizza) ... (several versions) This is a redo of a Problem from Lecture 10 and recently Lecture 132 if you didn't try it there. Nice review of the EditForm components InputRadioGroup, InputRadio and InputCheckbox.
PizzaStoreWebFormLecture10 (original WebForm version from Lecture 10)
CCpizzaBlazorWASM
CCpizzaBlazorWASMupdated
Adds more detailed Toppings check for special case discount (3 topping discount)
CCpizzaWasmHashSet (optional enrichment)
This version removes the use of Checkboxes for the Topping choices and instead uses an Unordered List which the user clicks on to highlight topping choices and implements a new more detailed Class for the Toppings with the use of a HashSet to store the selected toppings
public class ATopping
{
public string ToppingName { get; set; }
public double ToppingPrice { get; set; }
public string ToppingImage { get; set; }
public string Unicode { get; set; }
}
//This is the more detailed ATopping class being initialized with all the required values
//note the unicode assignments to emoji images
//Go here for more info https://www.w3schools.com/charsets/ref_utf_symbols.asp
List<ATopping> NewToppingOptions = new List<ATopping>()
{
new ATopping{ToppingName="Pepperoni", ToppingPrice=1.50,ToppingImage="pepperoni.png",Unicode="\u2600"},
new ATopping{ToppingName="Onions", ToppingPrice=.75,ToppingImage="onion.png", Unicode="\u2705"},
new ATopping{ToppingName="Green Peppers", ToppingPrice=.50,ToppingImage="greenpepper.png", Unicode="\u2668"},
new ATopping{ToppingName="Red Peppers", ToppingPrice=.75,ToppingImage="redpepper.png",Unicode="\u2622"},
new ATopping{ToppingName="Anchovies", ToppingPrice=2.0,ToppingImage="anchovie.png",Unicode="\u2b50"},
new ATopping{ToppingName="Mushrooms", ToppingPrice=.5,ToppingImage="mushroom.png",Unicode="\u261d"},
new ATopping{ToppingName="Sausage", ToppingPrice=2.5,ToppingImage="sausage.png",Unicode="\u23f0"},
new ATopping{ToppingName="Olives", ToppingPrice=1.0,ToppingImage="olive.png",Unicode="\u2764"}
};
//A HashSet is an optimized collection of unordered, UNIQUE elements
//that provides fast lookups
// ... similiar to a List
// ... BUT a List maintains the order of the elements as they are insert.
// A HashSet is an unordered collection and does not guarantee any specific sequence
HashSet<string> selectedToppings = new HashSet<string>();
private void OnToppingClicked(string top)
{
//Select Logic
//When the user clicks a fruit, we want to pass its name to this method
//This method will take the name and attempt to add it to the selectedFruits HashSet
//... Now remember HashSets do not allow duplicates
//... The HashSet Add method will take an item and attempt to add it
//if the item is already present, the call will return false and if it
//got added it'll return true.
if (!selectedToppings.Add(top))
{
selectedToppings.Remove(top);
}
}
Here is the HTML code for our unordered list of toppings
<ul>
@foreach (var topping in NewToppingOptions)
{
<li @onclick=" ( () => OnToppingClicked(topping.ToppingName))"
class="p-1 mb-1 @(selectedToppings.Contains(topping.ToppingName)? "bg-warning": null)"
style="font-size:15px; cursor:pointer">
@topping.ToppingName @topping.ToppingPrice.ToString("c")
<img src="images/@topping.ToppingImage" width="25" height="25"/>
@topping.Unicode
</li>
}
</ul>
BlazorStateManagementAnotherLook
Nice review of Dependency Injection to maintain State and Events.
DependencyInjectionIntroWASM2
Another nice application reviewing State Management. The technique you learn in this demo will allow you to pass a parameter value from one Blazor component (page) to another. Moreover, using a service class enables you to you to pass data in a way that does not require the use of routing parameters.
Here we have added the AppData class (from the Services folder) with a Singleton lifetime. Singleton is perfect for a client side Blazor app, but if you are working with Server-Side Blazor you will want to register as a Scoped service so each different user receives his/her own instance of the service for the duration of their session.
We look at two cases
Simple case ... passing data between two pages (Components)
Complex ... special case when you need to pass data between two components which are already loaded on the same page. In this case you will need to add an OnChange event to the service class and add an event handler to the component in order to tell the UI to refresh every time the service class property is changed.
@page "/page3"
@inject AppData appData
<h3>Page 3</h3>
@*Here we test out handing off of data between
Blazor components on the same page
*@
<InputComponent />
<DisplayComponent />
@code {
private string status = "";
protected override void OnInitialized()
{
//triggered when the OnChange event is raised
//You can attach any event handler you want to the
//appData.OnChange event
appData.OnChange += MyEventHandler;
}
private void MyEventHandler()
{
status="AppData changed.";
StateHasChanged();
}
}
InputComponent.razor
@inject AppData appData
<h3>Input Component</h3>
<input type="number" @bind="appData.Number" />
<br />
<br />
<select @bind="appData.Color">
<option value="#add8e6">Light Blue</option>
<option value="#90ee90">Light Green</option>
<option value="#d3d3d3">Light Grey</option>
<option value="#ffb6c1">Light Pink</option>
<option value="#fff">White</option>
</select>
<br />
<br />
DisplayComponent.razor
@inject AppData appData
<div style="width:300px; height:100px; background-color:@appData.Color">
<h3>Display Component</h3>
<p>You entered the number @appData.Number.</p>
</div>
@code {
protected override void OnInitialized()
{
//Assigns StateHasChanged() method as an event handler
//to be triggered when the service class's OnChange event
//is raised.
//Because appData is not being changed by DisplayComponent
//itself, the UI refresh must be manually triggered.
//This is why we added the OnChange event to the
//AppData service class
appData.OnChange += StateHasChanged;
}
}
AnotherToDoUpdated
Reviews the concept of Services ... using an Interface and Service (class) which implements the Interface
Lecture139ExtraExampleLocalStorageWASM (.NET 6)
In this application we will build a local storage service
The service will both write to and read from the browser's localStorage
We use an Interface and a Service (class) that implemens the Interface.
We will useJSinterop to accomplish this
Finally we will create a component to test our service
The Web Storage API for Javascript provides mechanisms for browsers to store key/value pairs. For each web browser, the size of data that can be stored in web storage is at least 5 mb per origin. The localStorage is defined in the Web Storage API for Javascript. We need to use JS interop to access localStorage on the browser. The browsers localStorage is scoped to a particular URL. If the user reloads the page or closes and re-opens the browser, the contents of localStorage are retained. If the user opens multiple tabs, each tab shares the same localStorage. The data in localStorage is retained until it is explicitly cleared since it does not have an expiration date
var myInterop = {};
myInterop.setLocalStorage = function (key, data) {
localStorage.setItem(key, data);
}
myInterop.getLocalStorage = function (key) {
return localStorage.getItem(key);
}
Lecture139LocalStorageWASMjsNET8 (.NET 8)
WordScramble
Classic Word Scramble game ... you are shown a scrambled 5 letter word and you have 5 chances to guess the word.
App loads in the entire English language 5 letter words and implements a keyboard created by Gregory Schier
Key teaching points
The downloaded text file (words.txt) is located in the sample-data folder under wwwroot
In the code section of our Index.razor page in the OnInitializedAsync() Task we will grab the entire file and store it in a string using Http.GetStringAsync () and then extract each 5 letter word from this list using the Split command
We create a simple grid of 1 row of 5 columns and right below it we implement a keyboard for guess entries.
Coding Highlights
First we implement a class called WordRow which has Guess a read-only property (no set... just get) that concatenates a string with all the letters in the guess ... $"{GuessedLetters[0]}{GuessedLetters[1]}{GuessedLetters[2]}{GuessedLetters[3]}{GuessedLetters[4]
The Reset() method kickstarts the game ... initializes some values ... gets a random word from the master list and then scrambles the word and finally creates an instance of the WordRow class with it's constructor initialized with the random word
protected string Scramble(string cword)
{
string scrambled = "";
while (cword.Length > 0)
{
int next = r.Next(0, cword.Length ); // Get a random number between 0 and 4 (remember generating rnd's r.Next(low,high+1))
scrambled += cword.Substring(next, 1); // Extract one of the letters from this random pos
cword = cword.Substring(0, next) + cword.Substring(next + 1); // Remove the character from the word and reconnect remaining string
}
return scrambled;
}
Now we start listening for clicks from the keyboard which represent what the player has keypressed for his/her unscrambled word guess
Three possible click scenarios are dealt with ...
If DEL key is pressed we have to backover (delete) letters ... decrement col counter and set GuessLetters[col]=""
If ENTER key is pressed ... time to check if answer is correct ... only allow the ENTER key(button) to be pressed when all 5 letters are entered (col counter =5 )... Also Player is allowed 5 attempts after which the answer is displayed
If ... any other key is pressed ... add the letter guess to the GuessedLetters[] array of the WordRow object ... add the letter to the HTML grid and move over one column
Notice how the keys turn black when we press them ... each key references KeyColor() which incorporates a DeadLetter string variable to track the current keys pressed ... if letter pressed in DeadLetters string use color black otherwise grey
NOTICE I HAVE LEFT IN THE ACTUAL ANSWER ON SCREEN FOR TESTING PURPOSES ... COMMENT THIS LINE WHEN YOU ARE READY TO PLAY THE GAME FOR REAL
PublicHolidayAPI (Server App version)
This App connects to an external API service which accepts a Country Code and Year and returns every Public holiday.
This application implements an Interface and related Service (class) to get all the Country codes and all the Holiday info.
BlazorMovieAppServer
Simple application which displays a set of movies and allows the user to click on any of them for a deeper look (review)
The MoviePage component displays a grid of movies, grouping them for visual clarity. It uses the IMovieReviewService to fetch movie data, which is provided by the MovieReviewService class. The service stores a list of MovieModel objects, each representing a movie with properties like title, description, image, and review.
MovieModel (Models folder): Defines the structure for movie data, including ID, title, description, image URL, and review text.
IMovieReviewService & MovieReviewService (Services folder): The interface (IMovieReviewService) specifies methods for retrieving movies and individual movie details. The implementation (MovieReviewService) provides hardcoded movie data and methods to access it.
MoviePage.razor: Fetches and displays movies using the service. It supports stream rendering for faster initial page loads and organizes movies in rows. It references the MovieItem component, passing each MovieModel as a parameter.
MovieItem.razor: Receives a MovieModel as a parameter and displays its details in a card layout. It provides a link to the MovieReview page using the movie's ID.
MovieReview.razor: Implements route parameters to display detailed reviews for a selected movie. It is referenced from MovieItem via a link that passes the movie ID in the route.
Together, these files enable browsing and reviewing movies in the app, with all data and logic managed through the service and model structure.
...Next step create a similiar application (Lectures 181-185) that accesses the real world API The Movie Database to fetch movie data dynamically.
In this Lecture we will
Learn that WebAssembly restricts apps from accessing a database. Therefore, you cannot perform Entity Framework Core database operations in your Blazor WebAssembly apps. However, there is another way to perform database operations, which is to create an ASP.NET Web API controller to wrap your database connection
Learn how to create an ASP.NET Core Hosted Blazor WebAssembly app which will allow us to create the required API controller
Briefly review the 3 different hosting models in Blazor
Blazor WebAssembly (Client Side)
Blazor Server (Server Side)
ASP.NET Core (improved version of the client side hosting model)
Recreate the ToDoList WebAssembly Application from Lecture 98 to illustrate these new concepts
Look over (BlazorToDoWebAssembly/ToDoStatusUpdate) as points of reference
Create a new WebAssembly App ... make sure to check ASP.NET Core Hosted (ToDoListAPI1)
This will create a multi-project application that we will use to separate the Blazor WebAssembly App from the ASP.NET Web API endpoints.
The hosted Blazor WebAssembly app includes three projects.
Client project (Almost identical to a typical WebAssembly app)
Server project (This is an ASP.NET Core project. This project is responsible for serving the application. In addition to hosting the client app, the server project provides the Web API endpoints)
Shared project (Also an ASP.NET Core project. It contains app logic that is shared between the other two projects like the model classes or validation code)
Let's start by clearing out a number of components of the default template solution
Delete all the components in the Client.Pages folder except for the Index page.
Delete the SurveyPrompt.razor page in the Client.Shared folder
Remove the About link from the Client.Shared\MainLayout.razor page
Remove the li elements for the Counter and Fetch data pages in the NavMenu.razor page
From the Server.Controller folder remove the WeatherForecastController.cs file
From the Shared Project delete the WeatherForecast.cs file
Rebuild the solution.
Next we add the TodoItem class to the Shared Project (ToDoListAPI2)
We are creating a Model-First migration with Entity Framework Core. This means we create what we want the database to look like in code. We create a model of the DB via a CLASS. The Entity Framework Core will turn that into an SQL Database.
public class TodoItem
{
public int Id { get; set; } //necessary for DB implementation
public string Title { get; set; }
public bool IsDone { get; set; }
}
Now we need to add an API controller for the TodoItem class.
Right click the Controllers folder and select Add,Controller option from the menu
Select API Controller with actions, using Entity Framework
Click the Add button
Set the Model class to TodoItem (ToDoListAPI.Shared)
Click the + button for the Add data context and then click the Add button to accept the default values
Controller name will appear by default ... Click Add to begin Entity Framework process
In the newly created TodoItemsController.cs update the route to [Route("[controller]")]
Setting up the SQL Server
We need to create a new sql database and add a table to it which will contain our Todo list.
First we modify the connection string in the appsettings.json file
"ConnectionStrings": {
"ToDoListAPIServerContext": "Server=(localdb)\\mssqllocaldb;Database=ToDoListDB;Trusted_Connection=True;MultipleActiveResultSets=true"
}
Now we go into the Tools menu ... NuGet Package Manager, Package Manager Console and execute
Add-Migration Init
Update-Database
The preceding commands used Entity Framework migrations to update the SQL Server ... let's go take a look at the DB
From the View menu select SQL Server Object Explorer
Click on localdb ... then Databases and you should see the ToDoListDB
Click the DB and then go inside the Tables folder
.. and finally click on dbo.TodoItem to view the table definition
Right click dbo.TodoItem and select the View Data option
Enter a few "Things to Do" ... don't enter anything into the ID field , it is automatically generated.
Time to make sure the app works so far ... start up the app
It will default to the Index page ... hopefully with no errors
... but we really want to make sure our Controller works so you will need to manually (for now) go up to the address bar and add TodoItems
It should return a simple JSON display of the current contents of the DB.
Next up ... we can now start working on our client project ... the UI
In this Lecture we will
Focus on creating the client project for our To Do List Application (ToDoListAPI3)
First let's focus on fetching the list of "Things to Do" and display them to the user.
Learn how to use the HttpClient service to call web APIs
Create a partial class (code behind) for the Index.razor page call it Index.razor.cs
don't forget to add the partial modifier before the word class
need to add using System.Net.Http.Json
here is where we will inject HttpClient
[Inject] public HttpClient Http { get; set; }
also make the following declarations
private IList<TodoItem> tasks;
private string error;
Now we code our OnInitializedAsync() method
//Here we are using the GetFromJsonAsync method to return the collection of TodoItems
protected override async Task OnInitializedAsync()
{
try
{
string requestUri = "TodoItems"; //api controller name
tasks = await Http.GetFromJsonAsync<IList<TodoItem>>(requestUri);
}
catch (Exception)
{
error = "Error Encountered";
}
}
Now let's go back up to our Index.razor page proper and start adding some of our HTML markup. Here's a skeleton of the basic structure.
@page "/"
@if (tasks==null)
{
<p><em>Loading ... </em></p>
}
else
{
@foreach(var todo in tasks)
{
}
}
Now let's make our To Do List actually appear and allow the user to mark a task complete by clicking the checkbox next to the name of the task. We change the color of the text box to indicate complete (green) or not completed (red) ... for now.
<ul>
@foreach(var todo in tasks)
{
<li>
<input type="checkbox" @bind="todo.IsDone" />
@{
if(todo.IsDone)
{
<input class="bg-success text-white" @bind="todo.Title" />
<button class="btn btn-outline-danger btn sm" title="Delete task">
<span class="oi oi-trash"></span>
</button>
}
else
{
<input class="bg-danger text-white" @bind="todo.Title" />
}
}
</li>
}
</ul>
So now the user can update the To Do List but the actual database is not getting updating. That's what we will deal with in the next Lecture.
In this Lecture we will
Focus on updating the database so we can edit, add or delete individual tasks in our To Do List. (ToDoListAPI4)
Learn how to use JSON helper methods to make requests in order to read, add, edit and delete data stored on an SQL server.
First we add an onchange event to our Checkbox which will call a method that updates the database with the current status of the task.
Unfortunately there is no way to combine a @bind and @onchange ... so we will need to edit our approach somewhat.
So we will do away with using the bind technique and rewrite our basic UI
<input type="checkbox" checked="@todo.IsDone" />
<span>@todo.Title</span>
<br />
<button class="btn btn-outline-danger btn-sm" title="Delete task" >
<span class="oi oi-trash"></span>
</button>
Next we will mark a task as complete by drawing a line across the task name when the user clicks the checkbox
First we create a simple style sheet class called ... completed
In the Pages folder create a style called Index.razor.css
Then enter the style class
.completed {
text-decoration:line-through;
}
Now we change the <span> markup where we have @todo.Title ... to
<span class= "@((todo.IsDone? "completed": ""))">
@todo.Title
</span>
... but if we test out the app right now the strikethrough will not show up ... we are now back to needing to implement the onchange event
We are going to add an onchange event to our checkbox which will call a method which will re-render the page and display the strike-through and update our DB.
<input type="checkbox" checked="@todo.IsDone"
@onchange="@(()=>CheckboxChecked(todo))"/>
private async Task CheckboxChecked(TodoItem task)
{
task.IsDone = !task.IsDone;
string requestUri = $"TodoItems/{task.Id}";
var response = await Http.PutAsJsonAsync<TodoItem>(requestUri, task);
if (!response.IsSuccessStatusCode)
{
error = response.ReasonPhrase;
};
}
Now lets code the Delete task (garbage can)
<button class="btn btn-outline-danger btn-sm"
title="Delete task"
@onclick="@(()=>DeleteTask(todo))">
<span class="oi oi-trash"></span>
</button>
private async Task DeleteTask(TodoItem task)
{
tasks.Remove(task);
string requestUri = $"TodoItems/{task.Id}";
var response = await Http.DeleteAsync(requestUri);
if (!response.IsSuccessStatusCode)
{
error = response.ReasonPhrase;
}
}
Let's finish off our app by incorporating the ability to add new tasks to our To Do list
First we will add an input and button above our list of To Do's
<input placeholder="Thing to do" @bind="newTodo" />
<button class="btn btn-success" @onclick="AddTask">Add To Do</button>
Then go into Index.razor.cs
private string newTodo;
private async Task AddTask()
{
if (!string.IsNullOrWhiteSpace(newTodo))
{
TodoItem newTaskItem = new TodoItem
{
Title = newTodo,
IsDone = false
};
tasks.Add(newTaskItem);
string requestUri = "TodoItems";
var response = await Http.PostAsJsonAsync(requestUri, newTaskItem);
if (response.IsSuccessStatusCode)
{
newTodo = string.Empty;
}
else
{
error = response.ReasonPhrase;
}
}
}
Extra Features
Let's have the Delete button (Garbage can) only appear when a task is complete
@if (todo.IsDone)
{
}
Let's add back our current count of "Things to Do" just above our Add button
<h3>To Do (@tasks.Count(todo => !todo.IsDone))</h3>
Supplementary Demos
ToDoListAPI4UpdatedA (Uses a Table formatted UI)
Here I have changed the display to a Table format with the headings Remove (thats where the delete ... garbage can will go) Description (things to do ) and Is Complete (check box)
<table class="table">
<thead>
<tr>
<th>Remove</th>
<th>Description</th>
<th>Is Complete</th>
</tr>
</thead>
<tbody>
@foreach(var todo in tasks)
{
<tr>
<td>
@if (todo.IsDone)
{
<button class="btn btn-outline-danger btn-sm"
title="Delete task"
@onclick="@(()=>DeleteTask(todo))">
<span class="oi oi-trash"></span>
</button>
}
</td>
<td>
<span class= "@((todo.IsDone? "completed": ""))">
@todo.Title
</span>
</td>
<td>
<input type="checkbox" checked="@todo.IsDone"
@onchange="@(()=>CheckboxChecked(todo))"/>
</td>
</tr>
}
</tbody>
</table>
ToDoListAPI4UpdatedB (Introduces column sorting)
This next version now adds the capability to sort by column (in this case Descriptions ... things to do) We create to methods SortTable to do the actual sorting and GetSortStyle is display an ascending and descending icon (bootstrap) depending on the current sort order ... it's activated right after the first time the column is clicked
<thead>
<tr>
<th>Remove</th>
<th @onclick="@(() => SortTable("Title"))" style="cursor:pointer">Description (sortable)
<span class="oi @(GetSortStyle("Title"))"></span>
</th>
<th>Is Complete</th>
</tr>
</thead>
//We need a field to tell us which direction the table is currently sorted by
//and a field to tell us which column the table is sorted by
private bool IsSortedAscending;
private string CurrentSortColumn;
private void SortTable(string columnName)
{
//Sorting against a column that is not currently sorted against
if (columnName!=CurrentSortColumn)
{
tasks = tasks.OrderBy(x =>
x.GetType()
.GetProperty(columnName)
.GetValue(x, null))
.ToList();
CurrentSortColumn = columnName;
IsSortedAscending = true;
}
else //Sorting against same column but in differnt direction
{
if (IsSortedAscending)
{
tasks = tasks.OrderByDescending(x =>
x.GetType()
.GetProperty(columnName)
.GetValue(x, null))
.ToList();
}
else
{
tasks = tasks.OrderBy(x =>
x.GetType()
.GetProperty(columnName)
.GetValue(x, null))
.ToList();
}
//Toggle boolean
IsSortedAscending = !IsSortedAscending;
}
}
private string GetSortStyle(string columnName)
{
if (CurrentSortColumn!=columnName)
{
return string.Empty;
}
if (IsSortedAscending)
{
return "oi-sort-ascending";
}
else
{
return "oi-sort-descending";
}
}
Lecture142ExtraExamples
EmployeeAppTableStriped
Simple Employee DB app which stores Employee Names, Emails and Phone Numbers with full CRUD ability where the Add and Edit/Delete options utilize separate Razor Page Components
Edit/Delete component uses route parameters
<tbody>
@foreach (var p in employeeList)
{
<tr>
<td>@p.FirstName @p.LastName</td>
<td>@p.Email</td>
<td>@p.MobileNo</td>
<td>
<a href="/editperson/@p.Id">Edit</a>
<a href="/editperson/@p.Id">Delete</a>
</td>
</tr>
}
</tbody>
EditPerson.razor
@page "/editperson/{Id}"
@using EmployeeApp.Shared
@inject HttpClient httpClient
@inject NavigationManager Nav
@inject IJSRuntime JsRuntime
[Parameter]
public string Id { get; set; }
Person person = new Person();
protected override async Task OnInitializedAsync()
{
person = await httpClient.GetFromJsonAsync<Person>("People/" + Id);
}
Employee Info Application (4 versions)
EmployeeInfoAppEventCallback
Database application that stores Ids, Names, Email and Experience in Years ... with full CRUD ability. It also implements EventCallbacks.
Incorporates some Javascript confirm "Are you Sure" when deleting a record.
The key difference/highlight in this version is the use of a
non-routable component (EmployeeForm) which will be referenced in the EmployeeCreate and EmployeeEdit routable components using Parameters unique to those pages.
EmployeeForm.razor
Here we are using the Parameter tag.
Recall...
What it does is allow external components to pass in these parameters. In our case we have defined the Developer object as a parameter
... So the other components ie (Edit/Create) that will use this
EmployeeForm component have an option to pass in an instance of the Developer object as a parameter. This allows this component to be used in several different ways.
We have also declared two Buttons that default to Save and Cancel but could be called anything you want ... and finally because the actual methods we called when we press these buttons are not located in this component but rather in the
calling component we declare to EventCallbacks
[Parameter]
public Developer dev { get; set; }
[Parameter]
public string ButtonText { get; set; } = "Save";
[Parameter]
public string ButtonTextCancel { get; set; } = "Cancel";
[Parameter]
public EventCallback OnValidSubmit { get; set; }
[Parameter]
public EventCallback OnInvalidSubmit { get; set; }
<EditForm Model="@dev" OnValidSubmit="@OnValidSubmit" @onreset="@OnInvalidSubmit">
<DataAnnotationsValidator />
<div class="form-group">
<label>First Name :</label>
<div>
<InputText @bind-Value="@dev.FirstName" />
<ValidationMessage For="@(() => dev.FirstName)" />
</div>
</div> .....
<button type="submit" class="btn btn-success">
@ButtonText
</button>
<button type="reset" class="btn btn-secondary">
@ButtonTextCancel
</button>
</EditForm>
EmployeeCreate.razor
@page "/employeecreate"
@inject HttpClient http
@inject NavigationManager Nav
<h3>Create New Employee Record</h3>
@*Here we make good use of the power of EmployeeForm component by
specifying parameters unique to Creating a new employee
Compare this to the Edit component
*@
<EmployeeForm ButtonText="Create New Employee" ButtonTextCancel="Cancel" dev="@emp"
OnValidSubmit="CreateEmployee" OnInvalidSubmit="HandleCancel" />
@code {
Developer emp = new Developer();
private async Task CreateEmployee()
{
await http.PostAsJsonAsync("developers", emp);
Nav.NavigateTo("employeespage");
}
private void HandleCancel()
{
Nav.NavigateTo("employeespage");
}
}
EmployeeInfoAppEventCallbackOrderBy
Sorts by Experience and then by LastName using LINQ Operators and a Lambda Expression.
<tbody>
@foreach (var dev in employees.OrderByDescending(m=>m.Experience).ThenBy(m=>m.LastName))
{
<tr>
<td>@dev.Id</td>
<td>@dev.FirstName</td>
<td>@dev.LastName</td>
<td>@dev.Email</td>
<td>@dev.Experience</td>
<td>
<a class="btn btn-success" href="employeeedit/@dev.Id">Edit</a>
<button class="btn btn-danger" @onclick="@(() => Delete(dev.Id))">Delete</button>
</td>
</tr>
}
</tbody>
EmployeeInfoAppSortFilterModalUpdate
Incorporates a Help Screen (using the Bootstrap modal class) that pop-ups on the first visit to the EmployeesPage.
<li class="nav-item px-3">
<NavLink class="nav-link" href="employeespage/0"> @*passing 0 to the EmployeePage.razor so that the Intro screen appears*@
<span class="oi oi-person" aria-hidden="true"></span> Employees
</NavLink>
</li>
Introduces a simple Search by LastName
<div class="col-sm-6" style="float:left">
<input class="form-control" type="text" placeholder="Search"
@bind="SearchString"
@bind:event="oninput" />
</div>
<tbody>
@foreach (var dev in employees)
{
if (!IsVisible(dev))
continue;
<tr class="@((dev.Experience>=5) ? "" : "table-danger")">
<td>@dev.Id</td>
<td>@dev.FirstName</td>
<td>@dev.LastName</td>
<td>@dev.Email</td>
<td>@dev.Experience</td>
<td>
<a class="btn btn-success" href="employeeedit/@dev.Id">Edit</a>
<button class="btn btn-danger" @onclick="@(() => Delete(dev.Id))">Delete</button>
</td>
</tr>
}
</tbody>
public bool IsVisible(Developer e)
{
if (string.IsNullOrWhiteSpace(SearchString))
return true;
if (e.LastName.Contains(SearchString, StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
EmployeeInfoAppSortFilterModalUpdateOrderBy
Sort by Experience Column (Clickable) ... Then by LastName
private void SortTable(string columnName)
{
if (columnName != activeSortColumn)
{
employees = employees.OrderBy(x => x.GetType().GetProperty(columnName).GetValue(x, null)).ThenBy(x=>x.LastName).ToArray();
isSortedAscending = true;
activeSortColumn = columnName;
}
else
{
if (isSortedAscending)
{
employees = employees.OrderByDescending(x =>
x.GetType().GetProperty(columnName).GetValue(x, null)).ThenBy(x=>x.LastName).ToArray();
}
else
{
employees = employees.OrderBy(x => x.GetType().GetProperty(columnName).GetValue(x, null)).ThenBy(x=>x.LastName).ToArray();
}
//flip to opposite state
isSortedAscending = !isSortedAscending;
}
}
FinancialEarningsTrackerModal
In this application we track Earnings by storing Dates, Earning Categories, Subject Specifics and Amount. We add additional earning on the right side of the main page Earnings.razor. This "Add New Earnings" is a non-routable component we reference.
Note the use of a non-routable component (EarningsForm ... located in Components folder) which incorporates Validation an EventCallback Parameter and references the class EarningModel.cs (located in Components folder)
Note: The class EarningModel.cs used by EarningForm.razor (Components folder) is almost identical to the class Earning.cs (Shared project) used in the Earnings.razor page (Pages folder ) EXCEPT that EarningModel.cs incorporates validation [Required] statements AND Earning.cs includes the field/property Id that is necessary to incorporate Entity Framework. They BOTH reference the enumeration EarningCategory located in the Finance.Shared project.
EarningForm will be referenced in the Earnings page ... a routable component
Note the use of a Modal Popup Dialog to confirm Record Deletion.
@if (DeleteDialogOpen)
{
<ModalDialog Title="Are you sure?" Text="Do you want to delete this entry" OnClose="@OnDeleteDialogClose" DialogType="ModalDialog.ModalDialogType.DeleteCancel"/>
}
Using local CSS definitions (CSS Isolation)
In MainLayout.razor.css
.sidebar { background-image: linear-gradient(180deg, #71818e 0%,#122d42 70%);}
OddOutGameMultiScreenAPInoDB (optional enrichment)
Simple game where you click on one of 4 images that don't belong
This application uses an API Controller but is NOT database related.
public class GameData : ControllerBase
{
// GET: api/<GameData>
[HttpGet]
public IEnumerable<GameImages> Get()
//public IEnumerable<string> Get()
{
List<GameImages> gamedata = new List<GameImages>();
gamedata.Add(new GameImages
{
id = "1",
Image1 = "A1_O.png",
Image2 = "A2_O.png",
Image3 = "A3_O.png",
Image4 = "A4_N.png",
Result = "4"
});
gamedata.Add(new GameImages
{
id = "2",
Image1 = "B1_O.png",
Image2 = "B2_N.png",
Image3 = "B3_O.png",
Image4 = "B4_O.png",
Result = "2"
});
gamedata.Add(new GameImages
{
id = "3",
Image1 = "C1_O.png",
Image2 = "C2_O.png",
Image3 = "C3_O.png",
Image4 = "C4_N.png",
Result = "4"
});
gamedata.Add(new GameImages
{
id = "2",
Image1 = "D1_O.png",
Image2 = "D2_N.png",
Image3 = "D3_O.png",
Image4 = "D4_O.png",
Result = "2"
});
return gamedata.AsEnumerable();
}
Note in the code section
GameImages[] gamesimg; //api call returns an array (list) of objects of type GameImages(declared in Shared project)
protected override async Task OnInitializedAsync()
{
gamesimg = await Http.GetFromJsonAsync<GameImages[]>("GameData"); //api call... located in Server project
}
void DisplayQuestion()
{
currentQuestionCount++;
//Here we are performing random selection without duplication
//using a flag technique
while(true)
{
randomQuestion = r.Next(0, questionCount); //this picks numbers between 0 ... questionCount-1
if (flag[randomQuestion]==0) //this question hasn't been picked so get out of loop
{
break;
}
}
flag[randomQuestion] = 1; //mark random selected question as picked
//store the names of the associated images for the array object at location randomQuestion in the array and the correct answer
Image1 = gamesimg[randomQuestion].Image1;
Image2 = gamesimg[randomQuestion].Image2;
Image3 = gamesimg[randomQuestion].Image3;
Image4 = gamesimg[randomQuestion].Image4;
ImageAnswer = gamesimg[randomQuestion].Result;
}
void FindAnswer(string checkvals)
{
//Here we are passing down either 1,2,3,4 .... ie @onclick=@(() => FindAnswer("3"))
//Compare passed down number (in quotes ... a string number) to the actual answer
if (checkvals==ImageAnswer)
{
totalPoints += 10;
}
else
{
totalPoints -= 5;
}
counterval++; //track how many questions we have answered so far
//... when we get to questionCount we are finished ... so display the results
if (counterval==questionCount)
{
DisplayResult();
return;
}
DisplayQuestion();
}
Using a series of if statements in the HTML section we simulate a mult-screen display
showGameStart ... displays Welcome screen
showGame ... displays actual game
showResult ... displays game results and play again.
In this Lecture we will
Revisit our new found knowledge of implementing a Database within a Blazor Webassembly application by recreating the "COVID Registration App" from Lecture 124 (CovidServerApp) ... and extend it's look and feel by borrowing from Lecture 125 (Registration)
We recreate a full CRUD implementation
Also serves as a nice review and extension of working with EditForm components when creating our UI and the subsequent built-in Validations
Get started with the App
Create a new WebAssembly App ... make sure to check ASP.NET Core Hosted (COVID ...COVID1InitialHostedWASM )
Let's start by clearing out a number of components of the default template solution
Delete all the components in the Client.Pages folder except for the Index page.
Delete the SurveyPrompt.razor page in the Client.Shared folder
Remove the About link from the Client.Shared\MainLayout.razor page
Change the li elements for the Counter and Fetch data pages in NavMenu.razor so they display "Add...To Be Vaxxed" and "Display Vaccinated". Don't change the href yet ... still to come
From the Server.Controller folder remove the WeatherForecastController.cs file
From the Shared Project delete the WeatherForecast.cs file
Rebuild the solution.
Add the classes, the API Controllers and Create the SQL Server database (COVID2DBsetupAPIController)
First we add the Vaccine class (including all the data Annotations ) to the Shared Project (use code snippet VaccineClass.txt)
public class Vaccine
{
public int Id { get; set; }
[Required]
public string Title { get; set; }
[Required]
[Display (Name ="First Name")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[Display(Name ="Date of Birth")]
public DateTime? DOB { get; set; }
//The question mark turns it into a nullable type
//which means that either it is a DateTime object or it is null
// DateOfBirth is of type Nullable<DateTime> rather than DateTime
// now calendar defaults to current date
[Required]
[StringLength(9, MinimumLength = 9, ErrorMessage = "Must be 9 alphanumeric")]
public string SIN { get; set; }
[Required(ErrorMessage = "Email is required")]
[DataType(DataType.EmailAddress)]
[EmailAddress]
public string Email { get; set; }
public bool Shot1 { get; set; }
public bool Shot2 { get; set; }
}
Next we add the API controller
Right click the Controllers folder in the COVID.Server project and select Add, Controller option and then choose API Controller with actions, using Entity Framework.
In the newly created controller update the route to [Route("[controller]")]
Setting up the SQL Server
We need to create a new sql database and add a table to it which will contain our Vaccine list.
First we modify the connection string in the appsettings.json file
"ConnectionStrings": {
"ToDoListAPIServerContext": "Server=(localdb)\\mssqllocaldb;Database=COVIDdb;Trusted_Connection=True;MultipleActiveResultSets=true"
}
Now we go into the Tools menu ... NuGet Package Manager, Package Manager Console and execute
Add-Migration Init
Update-Database
Recall : We are implementing a Model-First migration with Entity Framework Core. This means we create what we want the database to look like in code. We create a model of the DB via a CLASS. The Entity Framework Core will turn that into an SQL Database.
The preceding commands used Entity Framework migrations to update the SQL Server ... let's go take a look at the DB
From the View menu select SQL Server Object Explorer
Click on localdb ... then Databases and you should see the COVIDdb
Click the DB and then go inside the Tables folder
.. and finally click on dbo.Vaccine to view the table definition
Right click dbo.Vaccine and select the View Data option
Enter a few fictitious vaxxers ... don't enter anything into the ID field , it is automatically generated.
Time to make sure the app works so far ... start up the app
It will default to the Index page ... hopefully with no errors
... but we really want to make sure our Controller works so you will need to manually (for now) go up to the address bar and type Vaccines (name of controller)
It should return a simple JSON display of the current contents of the DB.
Next up ... we can now start working on our client project ... the UI
In this Lecture we will
Focus on the client project and create our Vaccination and Edit Pages for our Vaccination Registration Application
First let's go back to the index.razor page and add some images and a new welcome "Welcome To The COVID Vaccination Registration App"
Then let's start creating a page to display all the vaccinated individuals and their personal information (COVID3VaccinatedPage)
In the Pages folder create a page called VaccinePage.razor
Update the markup to the following
@page "/vaccinepage"
@using COVID.Shared
@inject HttpClient Http
Now add the code below
@code {
IList<Vaccine> vaccines;
protected override async Task OnInitializedAsync()
{
vaccines = await Http.GetFromJsonAsync<IList<Vaccine>>("Vaccines");
}
}
... and finally (see VaccinatedPage.txt)
<h2>Current Vaccinated List</h2>
@if (vaccines==null)
{
<p><em>Loading ...</em></p>
}
else if (vaccines.Count==0)
{
<div>None Found</div>
}
else
{
<table class="table">
<thead>
<tr>
<th></th>
<th>Title</th>
<th>First Name</th>
<th>Last Name</th>
<th>Date of Birth</th>
<th>SIN</th>
<th>Email</th>
<th>Shot 1</th>
<th>Shot 2</th>
</tr>
</thead>
<tbody>
@foreach (var item in vaccines)
{
@* Checking here for FULL Vaccinaton ... 2 shots
if NOT ... highlight entire record*@
<tr class="@((item.Shot1 && item.Shot2) ? "" : "table-danger")">
@* Here are we linking to the Edit page ... yet to be created
which will implement a 'route parameter' so that it will
render different views based on information in the URL
(such as person id)*@
<td>
<a href="/vaccine/@item.Id">Edit</a>
</td>
<td>@item.Title</td>
<td>@item.FirstName</td>
<td>@item.LastName</td>
<td>@item.DOB.Value.ToShortDateString()</td>
<td>@item.SIN</td>
<td>@item.Email</td>
<td>@(item.Shot1 ? "Yes": "No")</td>
<td>@(item.Shot2 ? "Yes" : "No")</td>
</tr>
}
</tbody>
</table>
}
Let's create a skeleton version of our Edit page which we will call VaccineEdit.razor . We will flesh out the form (EditForm) in the next lecture. (COVID4EditPage)
Start off by adding the following page directives
@page "/vaccine"
@page "/vaccine/{id:int}"
@* Note: Here we are using a route parameter (Lecture 90)
We want the same component to render different views based on information
in the URL (such as person id)
We will declare the Parameter id in the code section*@
@using COVID.Shared
@inject HttpClient Http
@inject NavigationManager Nav
@* This will be used to link back to the VaccinePage*@
Before we get any deeper into this page lets go back to the NavMenu.razor page and change the href to the proper component names... vaccine (for Add) and vaccinepage(for Display)
Now let's go back to the VaccineEdit.razor page and the code section.
@code {
[Parameter]
public int id { get; set; }
private bool ready;
private string error;
private Vaccine vaccine;
protected override async Task OnInitializedAsync()
{
if(id==0)
{
vaccine = new Vaccine();
}
else
{
vaccine = await Http.GetFromJsonAsync<Vaccine>($"Vaccines/{id}");
}
ready = true;
}
private async Task HandleValidSubmit()
{
HttpResponseMessage response;
if(vaccine.Id==0)
{
response = await Http.PostAsJsonAsync("Vaccines", vaccine);
}
else
{
string requestUri = $"Vaccines/{vaccine.Id}";
response = await Http.PutAsJsonAsync(requestUri, vaccine);
}
if (response.IsSuccessStatusCode)
{
Nav.NavigateTo( );
}
else
{
error = response.ReasonPhrase;
}
}
}
... and finally let's add our skeleton HTML markup which we will flesh out in the next lecture
<h3>Add/Edit Vaccination Information</h3>
@if (!ready)
{
<p><em>Loading ...</em></p>
}
else
{
}
In this Lecture we will
Complete the markup for the VaccineEdit.razor page by adding various input components to the EditForm Element. (COVID5EditForm ... recall Registration) .... else section (see EditPageHTML)
<div class="card m-3">
<h4 class="card-header">COVID Vaccination Registration Form</h4>
<div class="card-body">
<EditForm Model="@vaccine" OnValidSubmit="@HandleValidSubmit" @onreset="HandleReset">
<DataAnnotationsValidator />
<div class="form-row">
<div class="form-group col">
<label>Title</label>
<InputSelect @bind-Value="vaccine.Title" class="form-control">
<option value=""></option>
<option value="Mr">Mr</option>
<option value="Mrs">Mrs</option>
<option value="Miss">Miss</option>
<option value="Ms">Ms</option>
</InputSelect>
<ValidationMessage For="@(() => vaccine.Title)" />
</div>
<div class="form-group col-5">
<label>First Name</label>
<InputText @bind-Value="vaccine.FirstName" class="form-control" />
<ValidationMessage For="@(() => vaccine.FirstName)" />
</div>
<div class="form-group col-5">
<label>Last Name</label>
<InputText @bind-Value="vaccine.LastName" class="form-control" />
<ValidationMessage For="@(() => vaccine.LastName)" />
</div>
</div>
<div class="form-row">
<div class="form-group col">
<label>Date of Birth</label>
<InputDate @bind-Value="vaccine.DOB" class="form-control" />
<ValidationMessage For="@(() => vaccine.DOB)" />
</div>
<div class="form-group col">
<label>SIN/Health Card #</label>
<InputText @bind-Value="vaccine.SIN" class="form-control" />
<ValidationMessage For="@(() => vaccine.SIN)" />
</div>
</div>
<div class="form-row">
<div class="form-group col">
<label>Email</label>
<InputText @bind-Value="vaccine.Email" class="form-control" />
<ValidationMessage For="@(() => vaccine.Email)" />
</div>
<div class="form-group form-check">
<InputCheckbox @bind-Value="vaccine.Shot1" class="form-check-input" />
<label for="Shot1" class="form-check-label">Shot 1</label>
<ValidationMessage For="@(() => vaccine.Shot1)" />
</div>
<div class="form-group form-check">
<InputCheckbox @bind-Value="vaccine.Shot2" class="form-check-input" />
<label for="Shot2" class="form-check-label">Shot 2</label>
<ValidationMessage For="@(() => vaccine.Shot2)" />
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary mr-1">Save</button>
<button type="reset" class="btn btn-secondary">Cancel</button>
</div>
</EditForm>
</div>
</div>
<div>@error</div>
In the code section we need to create a method to handle pressing the Cancel button (@onreset)
private async Task HandleReset()
{
Nav.NavigateTo("vaccinepage");
}
Offer you several Challenges and Updates to the COVID application
COVID5EditFormInputSelect
Here we remove the hard-coded option values from our Dropdown menu (InputSelect) and store the values in a List which we cycle through via a foreach
List<string> titleOptions = new List<string> { "Mr", "Mrs", "Miss","Ms" };
<label>Title</label>
<InputSelect @bind-Value="vaccine.Title" class="form-control">
<option value=""></option>
@foreach (var opt in titleOptions)
{
<option value="@opt">@opt</option>
}
</InputSelect>
Add a Delete button to the edit page VaccineEdit.razor (COVID6Delete) ... Hint: Recall the garbage can from Lecture 142 (ToDoListAPI4)
<button class="btn btn-outline-danger btn-sm" title="Delete task" @onclick="@DeleteVaxxer">
<span class="oi oi-trash"></span>
</button>
private async Task DeleteVaxxer()
{
string requestUri = "Vaccines/" + vaccine.Id;
var response = await Http.DeleteAsync(requestUri);
if (response.IsSuccessStatusCode)
{
Nav.NavigateTo("vaccinepage");
}
else
{
error = response.ReasonPhrase;
}
}
Add a Search capability ie Filter to the VaccinePage.razor using an input field. Base the search on LastName. (COVID7SearchFilter)
<div class="form-group">
<input class="form-control" type="text" placeholder="Filter..."
@bind="Filter"
@bind:event="oninput">
</div>
@foreach (var item in vaccines)
{
if (!IsVisible(item))
continue;
Code Section
//used in the input field above
public string Filter { get; set; }
public bool IsVisible(Vaccine vax)
{
if (string.IsNullOrWhiteSpace(Filter))
return true;
if (vax.LastName.Contains(Filter, StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
Add a Column Sorting capability (Lastname or SIN) (COVID8ColSorting/COVID8ColSortingUpdated)
<tr>
<th></th>
<th>Title</th>
<th>First Name</th>
<th class= "sort-th" @onclick="@(() => SortTable("LastName"))">Last Name
<span class="fa @(SetSortIcon("LastName"))"></span>
</th>
<th>Date of Birth</th>
<th class= "sort-th" @onclick="@(() => SortTable("SIN"))">SIN
<span class="fa @(SetSortIcon("SIN"))"></span>
</th>
<th>Email</th>
<th>Shot 1</th>
<th>Shot 2</th>
</tr>
Code Section
//Here we declare a boolean which will flip between the state of
//sorted in ascending order and NOT sorted in ascending order (descending)
//The second variable stored the current column we are sorting by
private bool isSortedAscending;
private string activeSortColumn;
private void SortTable(string columnName)
{
if (columnName != activeSortColumn)
{
vaccines = vaccines.OrderBy(x =>
x.GetType().GetProperty(columnName).GetValue(x, null)).ToList();
isSortedAscending = true;
activeSortColumn = columnName;
}
else
{
if (isSortedAscending)
{
vaccines = vaccines.OrderByDescending(x =>
x.GetType().GetProperty(columnName).GetValue(x, null)).ToList();
}
else
{
vaccines = vaccines.OrderBy(x =>
x.GetType().GetProperty(columnName).GetValue(x, null)).ToList();
}
//flip to opposite state
isSortedAscending = !isSortedAscending;
}
}
Add an Animated Loading GIF (COVID9AnimatedLoadingGIF)
@if (vaccines==null)
{
<div class="text-center">
<p><em>Loading ...</em></p>
<img src="images/loading.gif"/>
</div>
}
Use only one menu with Add and Edit all on same page (COVID10UpdatedAdd)
<div class="row mt-4">
<div class="col-6">
<h4 class="card-title text-primary">Current Vaccinated List</h4>
</div>
<div class="col-4 offset-2">
<a href="vaccine" class="btn btn-info form-control">Add New Vaxxer</a>
</div>
</div>
Add two new calendars (VaccineEdit Page) to post dates of vaccinations (determine difference between dates ... number of days and whether they meet eligibility) (COVID10UpdatedAddCalendars1)
//New update ... used to determine number of days between vaccinations
//DateTime.Now added to deal with possible null values when saving/updating
//Database
[Display(Name = "Date of 1st Vaccination")]
public DateTime? DOB1 { get; set; } = DateTime.Now;
[Display(Name = "Date of 2nd Vaccination")]
public DateTime? DOB2 { get; set; } = DateTime.Now;
<div class="form-group col">
<label>Date of First Vaccination</label>
<InputDate @bind-Value="vaccine.DOB1" class="form-control" />
<ValidationMessage For="@(() => vaccine.DOB1)" />
</div>
<div class="form-group col">
<label>Date of Second Vaccination</label>
<InputDate @bind-Value="vaccine.DOB2" class="form-control" />
<ValidationMessage For="@(() => vaccine.DOB2)" />
</div>
@if (vaccine.DOB1!=null & vaccine.DOB2!=null)
{
if ((vaccine.DOB2 - vaccine.DOB1).Value.Days >= 0)
{
<div class="alert-info">
@((vaccine.DOB2 - vaccine.DOB1).Value.Days)
Days Between Vaccinations
</div>
if ((vaccine.DOB2 - vaccine.DOB1).Value.Days <180)
{
<div class="alert-danger"> Too Early for 2nd Vaccination (at least 180 days required)</div>
}
else
{
<div class="alert-primary">2nd Vaccination Allowed </div>
}
}
}
Add two new calendars to VaccinePage that are used to Filter all people who received their First Vaccination between a range of dates COVID10UpdatedAddCalendars2
<div class="input-group" style="float:left">
<label class="alert-info">From:</label><input class="form-control" type="date" @bind="From" />
<label class="alert-info">To:</label><input class="form-control" type="date" @bind="To" />
</div>
<div>
<button class="btn btn-success" @onclick="@(() => lookupDates(From,To))">Lookup All Shot 1 Vax Between ... </button>
<button class="btn btn-success" @onclick="Clear">Clear</button>
</div>
Code Section
//This is a NEW flag to recognize whether we are filtering by LastName
//... or Dates (using calendar input)
private bool dateFlag = false;
//stores filtered records for individuals who have received shot 1 between From and To dates
IList<Vaccine> calvaccines = new List<Vaccine>();
//used for the calendars inputs above
public DateTime From { get; set; } = DateTime.Now;
public DateTime To { get; set; } = DateTime.Now;
private void lookupDates(DateTime f, DateTime t)
{
calvaccines.Clear();
@foreach (var item in vaccines)
{
if (item.DOB1>=f && item.DOB1<=t)
{
calvaccines.Add(item);
}
}
dateFlag = true;//used in the HTML above to determine which display to use
}
HTML
@if (dateFlag==false)
{
@foreach (var item in vaccines)
{
if (!IsVisible(item))
continue;
......
else
{
@foreach (var item in calvaccines)
{
COVID10UpdatedAddCalendars3NoSun
Vaccinations on Saturday or Sunday not available
@if (vaccine.DOB1 != null & vaccine.DOB2 != null )
{
if (vaccine.DOB1.Value.DayOfWeek != DayOfWeek.Sunday & vaccine.DOB2.Value.DayOfWeek!=DayOfWeek.Sunday)
{
if ((vaccine.DOB2 - vaccine.DOB1).Value.Days >= 0)
{
<div class="alert-info">
@((vaccine.DOB2 - vaccine.DOB1).Value.Days)
Days Between Vaccinations
</div>
if ((vaccine.DOB2 - vaccine.DOB1).Value.Days <180)
{
<div class="alert-danger"> Too Early for 2nd Vaccination (at least 180 days required)</div>
}
else
{
<div class="alert-primary">2nd Vaccination Allowed </div>
}
}
}
else
{
<div class="alert-danger">No Vaccinations available Sunday </div>
}
}
Supplementary Demos and Suggested Exercises
RadzenBlazorComponents3DatePicker
Demo of the DatePicker in Radzen Blazor ... a followup to the COVID App Calendar implementation from above
Basic DatePicker (calendar)
DatePicker with time
DatePicker with highlighted and disabled dates (Sat/Sun)
Recreate the entire SongList Database application (Blazor Server App) from Lectures 108-111 into a WebAssembly Application (SongListAppServerAndWASM)
BlazorSongList4 (original Blazor Server App from Lectures 108)
SongListWASM
New version tracks Song Title, Artist, Year and whether it Charted at #1. Number one's are highlighted in red.
You can click on a row instead of pick Edit
<tbody>
@foreach (var item in songs.OrderBy(m=>m.Year))
{
if (!IsVisible(item))
continue;
<tr class="@(!item.Chart1 ? "" : "table-danger")" @onclick="@(() => HandleEdit(item.Id))" >
<td>
<a href="/song/@item.Id">Edit</a>
</td>
<td>@item.Title</td>
<td>@item.Artist</td>
<td>@item.Year</td>
<td>@(item.Chart1 ? "Yes": "No")</td>
</tr>
}
</tbody>
private async Task HandleEdit(int id)
{
string requestUri = "song/" + id.ToString();
Nav.NavigateTo(requestUri);
}
SongListWASMUpdated
Number one's have a star beside the name
<td>@item.Title
@if(item.Chart1)
{
<i class="oi oi-star " style="color:red"></i>
}
</td>
You can click on a row and highlight it.
Song.cs class updated
//added to Updated app version to handle highlighting a table row by clicking on it
public bool IsClicked { get; set; } = false;
<tr class="@(!item.IsClicked ? "" : "table-danger")" @onclick="@( () => item.IsClicked=!item.IsClicked)">
Recreate the Raptors Database application from either Section 7 or 8 (BlazorRaptorsDB) ... start off with a basic solution and then enhance the UI by incorporating the Radzen Blazor DataGrid
BlazorRaptorsDB
Basic CRUD implementation which stores PlayerNumbers, PlayerNames, Image (InputFile used for entry) , Position, Height,Salary, College
Position and Salary are sortable
Filter by Number,Name or Position (Text entry)
Filter by Position (DropDown List)
private string[] AllPositions = { "C Center", "PF Power Forward", "PG Point Guard", "SF Small Forward", "SG Shooting Guard" };
<select class="form-select" @bind="Filter">
<option value="">Pick Positions from List</option>
@foreach (var pos in AllPositions)
{
<option value="@pos">@pos</option>
}
</select>
public bool IsVisible(Player p)
{
if (string.IsNullOrWhiteSpace(Filter))
return true;
if (p.PlayerNumber.Contains(Filter, StringComparison.OrdinalIgnoreCase))
return true;
if (p.PlayerName.Contains(Filter, StringComparison.OrdinalIgnoreCase))
return true;
if (p.PlayerPosition.Contains(Filter, StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
Note a Second method used in the RaptorsEdit page to display the contents of the DropDown . Here we make the use of a text file located in the wwwroot/sample-data folder
C Center
PF Power Forward
SF Small Forward
PG Point Guard
SG Shooting Guard
private string[]? AllPositions;
private List<string> sortedPositions;
protected override async Task OnInitializedAsync()
{
var wordString = await Http.GetStringAsync("sample-data/positions.txt");
AllPositions = wordString.Split("\n");
//Here we are removing the extra character \r ... last line we don't touch it is a blank line we will deal
//with after transferring the array contents to a LIST
for (int i=0;i<AllPositions.Length-1;i++)
{
AllPositions[i] = AllPositions[i].Substring(0, AllPositions[i].Length - 1);
}
sortedPositions = AllPositions.OrderBy(pos => pos).ToList(); //alpha sort ascending
sortedPositions.RemoveAt(0); //removes the blank line that gets sorted up to the top
if (id==0)
{
player = new Player();
}
else
{
player = await Http.GetFromJsonAsync<Player>("Players/" + id);
ProfilePicDataUrl = player.PlayerImage;
}
ready = true;
}
<InputSelect @bind-Value="player.PlayerPosition" class="form-select">
<option value="0" disabled="disabled" selected>Select Position</option>
@foreach (var pos in sortedPositions)
{
<option value="@pos">@pos</option>
}
</InputSelect>
Note the use of the modal class to display a popup which prompts "Are you Sure?" for Deletions
Note on the RaptorsPage that a total payroll is calculated at the bottom of the table
<tr class="table-primary">
<td>Total Payroll:</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>@total.ToString("c2")</td>
<td></td>
</tr>
BlazorRaptorsDBdropdownUpdate
minor update
<select class="form-select" @bind="Filter">
Radzen Implementations (Start Without Debugging)
BlazorRaptorsDBRadzen
BlazorRaptorsDBRadzenUpdatedCount
BlazorRaptorsDBRadzenUpdatedSwitch
BlazorRaptorsUpFiltFooterTotals
In this Lecture we will
Discuss how a Blazor WebAssembly application can also work offline as a Desktop or Mobile app.
This type of application is called a Progressive Web Application (PWA)
PWA is Single Page Application (SPA) that uses modern browser APIs and capabilities to behave like a desktop app. It's an enhanced version of a web app. They run in their own app window instead of the browser's window and they can be launched from the Start menu or desktop.
They offer an offline experience and load instantly due to their use of caching
They receive push notifications and are automatically updated in the background
PWA is trying to bridge the gap between native and web apps. It is not a Microsoft Technology it is a Web Technology
Learn how to create a simple PWA with Blazor WebAssembly (BlazorPWA)
We start Visual Studio 2019/2022 as usual and choose WebAssembly App not Server and make sure to check off all three boxes
Configure for HTTPS
ASP.NET Core Hosted (this backend server project is not necessary but helps give us access to some PWA features when the app is published)
Progressive Web Application
Project Structure
Nothing has changed under Pages and Shared folders or Imports.razor, App.razor or Program.cs
The place where we do have changes in the files for PWA is under the wwwroot folder
We can see four new files icon-512.png, manifest.json, service-worker.js and service-worker.published.js
icon-512.png will be the default icon for our desktop app
manifest.json is the heart of the PWA. It is a simple JSON file. Here is where we declare and setup the app. It contains the application name, defaults and startup parameters. It describes how the application looks and feels. We can modify and add more data to this file.
{
"name": "BlazorPWA",
"short_name": "BlazorPWA",
"start_url": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#03173d",
"icons": [
{
"src": "icon-512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}
service-worker.js is a special kind of Javascript file that aids in how our PWA functions offline. It intercepts and controls how the browser handles its network requests and caching. It is separate from the app and has no DOM access. It runs on a different thread used by the main Javascript that powers your app, so it is not blocking. It is designed to be fully asynchronous.
// In development, always fetch from the network and do not enable offline support.
// This is because caching would make development more difficult (changes would not
// be reflected on the first load after each change).
self.addEventListener('fetch', () => { });
This is the default service worker that Microsoft includes in the template and is all that is technically needed to be a PWA ... but the published version below is used after development
service-worker.published.js is used after the app is published.
There are three steps in the life cycle of a service worker:
Install
service worker usually caches some of the static assets of the website like splash screens. If the files are cached successfully, the service worker is installed. After the install step is completed the activate step is initiated.
Activate
Here the worker handles the management of old caches. Since a previous install may have created a cache, this is an opportunity to delete it. After the activate step is successfully completed the service worker is ready to begin processing the fetch events.
Fetch
During the fetch step, the service worker controls all the pages that fall under its scope. It will handle the fetch events that occur when a network request is made from the PWA. The service worker will continue to fetch until it is terminated.
index.html contains the details that tells the browser that this application is a PWA.
Run the application
After running , the application still looks like a normal Blazor app. The difference is tiny. If you look into the URL area you should see a tiny icon (if the current PWA has not been installed before) that looks like a down arrow (in Chrome). Once you click it , it will prompt to install the app .
Note: If using Visual Studio 2022 or greater you MAY need to choose Start without Debugging for install prompt to appear
Once installed the browser application will be closed and the desktop application will be opened. This is our Blazor app, but now running as a PWA. It does not have any browser URL, navigational bars and it just looks like a desktop app.
Notice also a shortcut icon of our app in now also on the desktop
Key Notes:
IISExpress must still be running for the app icon to work. In development mode PWA will NOT work in Offline mode. Offline support would interfere with local development since you might be viewing cached offline files when you make updates.
Offline support is only available for published applications.
To test this out locally, we will need to publish our app. (BlazorPWAPub)
Right click on the BlazorPWA.Server project and select Publish to folder.
After the process is complete you need to go into the folder indicated in the publish script and start up BlazorPWA.Server.exe
This launches the published application running on a local host port. Make note of the https address ... https://localhost:5001
Start up your browser and enter the address above. This should start up your application ... Now re-install it.
This will again place a short-cut icon on the desktop. This version will work Offline with no need for IISExpress to be running in the background.
Updated Progressive Web App Creation ... using Visual Studio 2022/2026 (BlazorPWApub-2026)
The most recent updates to VS 2022/2026 now use the WebAssembly Standalone Template instead of the WebAssembly Hosted Template (no longer exists) to create a Progressive Web App.
For a basic PWA there really is no need for the Server project ... UNTIL you need full offline capabilities. Then before you Publish you will need to add a Server Project ie ... ASP.NET Core Web API project.
To test the application out locally we need to publish the application. Right click on the API project and select Publish. Then go to the publish folder and run BlazorPWA.api exe. Then open a browser and navigate to https://localhost:5001.
Now you are running the PWA locally and can install it. This version of the PWA will have offline capabilities ie. you can turn off your internet connection , stop running Visual Studio and the app will still work.
Customizing PWA ... manifest.json
Change the PWA name . Short name is used in the app launcher once your PWA is installed. The long name is used in the install dialog.
Change the icon. If you name the image icon-512.png it will be automatically discovered during the build process and populated in manifest.json
Learn when to create a PWA
First realize that a PWA does not go into an app store it's not exactly a full fledged app. It's kind of an intermediate step. It's not a Website or a Mobile app or a Desktop app, it sits in the middle. It's best used when ...
We can provide data even when we are offline
Your application will be used frequently and you don't want to type a URL
Although Blazor PWA apps can easily be created, there are tradeoffs. Because there's no .NET API support for service workers, all functionalities must be done in JavaScript. And because one of Blazor's attractions is Csharp, this deters some developers from venturing too deep into service workers.
Want to take a deeper dive ... Check out the Resource Links
PWA Features Benefits and Drawbacks
Blazor PWA vs Blazor MAUI : Which one is right for your project ?
The Ultimate Guide To Progressive Web Apps in 2024 – with 50 PWA Examples
.... In Lectures 166-170
We learn that Blazor Hybrid (.NET MAUI Blazor) is both a Windows Desktop (a step above PWA) and Mobile creating application. With Blazor Hybrid, the primary goal has shifted by extending the capabilities of .NET developers beyond the Web into desktop and mobile development In a Blazor Hybrid app, Razor components run natively on the device. .... BUT the Blazor Hybrid App can also be modified by adding a Blazor Server or WebAssembly project to become a basic Web Application.
Offer you the Challenge to convert the Currency Exchange problem from Lecture 93 into a PWA
CurrenyExchangeFromLecture93
CurrencyExchangePWA
CurrencyExchangePWAPub
In this Lecture we will
Begin the process of creating a local 5 day weather forecast application that can be installed and run as a native application . We will use Javascript's Geolocation API to obtain the location of our device and use the OpenWeather API to fetch the weather forecast for our location.
We will be using an external data service Open Weather which implements a API that returns data in JSON format. This is a free API but in order to get started with it you need to create an account at https://openweathermap.org and then obtain an API key.
Choose Pricing ... Free ... Get API key
Create our base Blazor WebAssembly application which implements HTTPS ,ASP.NET Core Hosted and Progressive Web Application (LocalWeatherForecast1)
Our first step is to determine our current location's latitude and longitude which we will feed to the Open Weather API to determine the local weather. This will involve using the Geolocation API.
The Geolocation API is accessed through a navigator.geolocation object (via Javascript). When we make this call, the user's browser asks for permission to access their location.
Before we attempt to use the navigator.geolocation object we need to verify that it is supported by the browser.
Then we access the object returning coords.latitude and coords.longitude properties.
So we create a folder called scripts in the wwwroot folder and create a new javascript file which we will call myscript (GeolocationAPImyscript.txt)
var myLocation = {};
myLocation.getPosition = async function () {
function getPositionAsync() {
return new Promise((success, error) => {
navigator.geolocation.getCurrentPosition(success, error);
});
}
if (navigator.geolocation) {
var position = await getPositionAsync();
var coords = {
latitude: position.coords.latitude,
longitude: position.coords.longitude
};
return coords;
}
else {
throw Error("Geolocation is not supported by this browser.");
};
}
Don't forget to add a reference to the Javascript file in the index.html page
script src="scripts/myscript.js"
Confused with the Javascript code ... don't worry Blazor Hybrid (.NET MAUI Blazor) gives Blazor access to all of the .NET MAUI's platform API given us a Javascript free and simpler implementation. We revisit GeoLocation again in Lecture 167.
Now lets invoke our myLocation.getPosition function from our web app.
First we create a class called Position to represent the latitude and longitude in a folder called Models.
public class Position
{
public double Latitude { get; set; }
public double Longitude { get; set; }
}
Now lets go to our Index.razor page and see if we can make our location show up . This display of latitude and longitude is just a test, later it will be incorporated in our call to the Open Weather API.
@page "/"
@using LocalWeatherForecast.Client.Models
@inject IJSRuntime js
@if (pos==null)
{
<p><em>@message</em></p>
}
else
{
<h2>Latitude: @pos.Latitude, Longitude: @pos.Longitude</h2>
}
@code {
string message = "Loading ...";
Position pos;
protected override async Task OnInitializedAsync()
{
//We attempt to get the coordinates when the page initializes
try
{
await GetPosition();
}
catch (Exception)
{
message = "Geolocation is not supported";
}
}
private async Task GetPosition()
{
//Recall calling Javascript Functions (Lecture 116)
pos = await js.InvokeAsync<Position>("myLocation.getPosition");
}
}
Run the app
Observe the prompt for permission to access your location.
We now know our location, so next up we need to provide these coordinates to the Open Weather API
In this Lecture we will
Continue developing our Local Weather Forecast application by focusing in on prepping our app (via classes and components) to receive the data response from the Open Weather API when we connect to it with our location coordinates (LocalWeatherForecast2)
The data source for our app specifically comes from the Open Weather One Call API
The One Call API 2.5 has been deprecated on June 2024. Please read the detailed guide for transitioning to the more advanced One Call API 3.0 (How to transfer from One Call API 2.5 to the One Call API 3.0) located in the Resources
Please note, the One Call API 3.0 subscription requires credit card details. We use your payment card details only for those calls that go beyond the free limit. If you do not want to exceed a free limit you can always set a daily threshold for your account.
This API is able to return the current forecast. We will be using it to access the local forecast for the next 5 days.
This is the format of the API
https://api.openweathermap.org/data/2.5/onecall?lat={lat}&lon={lon}&exclude={part}&appid={API key}
Here is an example API call
https://api.openweathermap.org/data/2.5/onecall?lat=33.44&lon=-94.04&exclude=hourly,daily&appid={API key}
This is a fragment of the response from the API that we will focus on. We will use a number of these fields.
"daily": [ {
"dt": 1618308000,
"sunrise": 1618282134,
"sunset": 1618333901,
"temp": {
"day": 279.79,
"min": 275.09,
"max": 284.07,
"night": 275.09,
"eve": 279.21,
"morn": 278.49
},
"feels_like": {
"day": 277.59,
"night": 276.27,
"eve": 276.49,
"morn": 276.27 },
"pressure": 1020,
"humidity": 81,
"dew_point": 276.77,
"wind_speed": 3.06,
"wind_deg": 294,
"weather": [
{
"id": 500,
"main": "Rain",
"description": "light rain",
"icon": "10d" } ],
"clouds": 56,
"pop": 0.2,
"rain": 0.62,
"uvi": 1.93
},
First we need to add a new class which will mirror the fields we wish to capture from the API.
In the models folder add a class called OpenWeatherForecast
public class OpenWeatherForecast
{
public Daily[] Daily { get; set; }
}
public class Daily
{
//Dt is current time in Unix format
public long Dt { get; set; }
public Temp Temp { get; set; }
public Weather[] Weather { get; set; }
}
public class Temp
{
public double Min { get; set; }
public double Max { get; set; }
}
public class Weather
{
public string Description { get; set; }
public string Icon { get; set; }
}
Next we create a non-routable razor component which will be referenced in the Index.razor page when we display the weather for the next 5 days.
Create a razor component called DailyWeather in the Shared folder . See code snippet (DailyWeatherRazor.txt)
<div class="card text-center">
<div class="card-header">
@Date
</div>
<div class="card-body">
<img src="@IconUrl" />
<h4 class="card-title">@Description</h4>
<b>@((int)HighTemp) F°</b> /
@((int)LowTemp) F°
</div>
</div>
@code {
[Parameter] public long Seconds { get; set; }
[Parameter] public double HighTemp { get; set; }
[Parameter] public double LowTemp { get; set; }
[Parameter] public string Description { get; set; }
[Parameter] public string Icon { get; set; }
private string Date;
private string IconUrl;
protected override void OnInitialized()
{
//This will take Dt value (Unix Time) from the API response
//which was assigned to the variable Seconds and convert to full regular date.
//Sunday November 28 2021
Date = DateTimeOffset
.FromUnixTimeSeconds(Seconds)
.LocalDateTime
.ToLongDateString();
//This will take the value of the Icon received from the API response
//which will be assigned to the variable Icon and concatenates it to the URL expression below at {0}
IconUrl = String.Format("https://openweathermap.org/img/wn/{0}@2x.png", Icon);
//alternate format
//IconUrl = "https://openweathermap.org/img/wn/" + Icon + "@2x.png";
}
}
In this Lecture we will
Complete the Weather App by finally connecting to the Open Weather One Call API and fetching a 5 day forecast based on our location. (LocalWeatherForecast3)
Change from the development environment to published version of our PWA.
Focus on the Index.razor page
Add using and injection statements to be used to help us connect to the API
@using System.Text
@inject HttpClient Http
In the code section add the following statements and methods
OpenWeatherForecast forecast
private async Task GetPosition()
{
//This returns the latitude and longitude of our current position
pos = await js.InvokeAsync<Position>("myLocation.getPosition");
//now that we have our current position coordinates we can call the
//Open Weather API to determine the weather forecast
await GetForecast();
}
private async Task GetForecast()
{
//recall the format of an API call
//https://api.openweathermap.org/data/2.5/onecall?lat={lat}&lon={lon}&exclude={part}&appid={APIkey}
string APIKey = "518415d369a87b293cb010827b9e79db";
StringBuilder url = new StringBuilder();
url.Append("https://api.openweathermap.org");
url.Append("/data/2.5/onecall?");
url.Append("lat=");
url.Append(pos.Latitude);
url.Append("&lon=");
url.Append(pos.Longitude);
url.Append("&exclude=");
url.Append("current,minutely,hourly,alerts");
url.Append("&units=imperial");
url.Append("&appid=");
url.Append(APIKey);
forecast = await Http
.GetFromJsonAsync<OpenWeather>
(url.ToString());
}
Now we display the weather forecast for the next 5 days ... add the code below in the else section of the HTML
<div class="card-group">
@* Take a specified number of contiguous elements from the start of the sequence
... so we loop through the forecast object five times*@
@foreach (var item in forecast.Daily.Take(5) )
{
<DailyWeather
Seconds="@item.Dt"
LowTemp="@item.Temp.Min"
HighTemp="@item.Temp.Max"
Description="@item.Weather[0].Description"
Icon="@item.Weather[0].Icon"
/>
}
</div>
Publish the application to properly implement the offline PWA version
The default in Blazor PWAs is for the service-worker to follow a cache first strategy.
If the requested page is in the cache, it serves that page before it requests the page from the server and updates the cache with the new page. Using this service worker, we always serve the version of the page that is in the cache before requesting the page from the server, thus users are served the same data whether they are online or offline.
Notice Fetch Data page will not work. Requests to backend Web API will fail due to lack of network.
Check the weather the next day ... it will work !
Change the display units to Celsius (LocalWeatherForecast4PWAmetric)
Connect to Google Maps (LocalWeatherForecast3GoogleMaps)
Supplementary Demo/Exercise
ExchangeRateExercise.pdf
Your objective is to create a simple PWA that will connect to an external API services that tracks
Global Exchange Rates. Your app will allow the user to enter the Country of their choice (via a
Dropdown list) and it’s associated Currency Code (eg US is USD, Canada is CAD) and have it
display the value of that currency against all the other currencies.
ExchangeRatePWAappSolutions
ExchangeRatesAPI
Note the use of the json file currencies.json ... this will be used in the DropDown List (select HTML tag) when picking Base Country Monetary Code
[
{
"CurrencyCode": "AED",
"CurrencyName": "United Arab Emirates Dirham"
},
{
"CurrencyCode": "ARS",
"CurrencyName": "Argentine Peso"
},
public class Country
{
public string CurrencyCode { get; set; }
public string CurrencyName { get; set; } //https://developers.google.com/adsense/management/appendix/currencies
//... converted from csv to json and contents placed locally in wwwroot folder
//under the sample-data folder (currencies.json)
}
Note the use of a Dictionary class when working with the actual conversion rates
public class Rate
{
//public string result { get; set; }
//public string documentation { get; set; }
//public string terms_of_use { get; set; }
//public string time_last_update_unix { get; set; }
public string time_last_update_utc { get; set; }
//public string time_next_update_unix { get; set; }
//public string time_next_update_utc { get; set; }
public string base_code { get; set; }
public Dictionary<string,double> conversion_rates { get; set; }
//Here is an example of the JSON response
//from the ExchangeRate-API
//https://v6.exchangerate-api.com/v6/e0cd659d834c4d43ace77844/latest/USD
// {
//"result": "success",
//"documentation": "https://www.exchangerate-api.com/docs",
//"terms_of_use": "https://www.exchangerate-api.com/terms",
//"time_last_update_unix": 1585267200,
//"time_last_update_utc": "Fri, 27 Mar 2020 00:00:00 +0000",
//"time_next_update_unix": 1585353700,
//"time_next_update_utc": "Sat, 28 Mar 2020 00:00:00 +0000",
//"base_code": "USD",
//"conversion_rates": {
// "USD": 1,
// "AUD": 1.4817,
// "BGN": 1.7741,
// "CAD": 1.3168,
// "CHF": 0.9774,
// "CNY": 6.9454,
// "EGP": 15.7361,
// "EUR": 0.9013,
// "GBP": 0.7679,
// "...": 7.8536,
// "...": 1.3127,
// "...": 7.4722, etc.etc.
//}
}
Code Section
@code {
Rate rates = new Rate();
string CurrencyCode;
//List used for DropDown component in UI
List<Country> countries = new List<Country>();
string feedback = "";
protected override async Task OnInitializedAsync()
{
countries = await Http.GetFromJsonAsync<List<Country>>("sample-data/currencies.json");
}
private async void TheButtonClicked()
{
try
{
rates = await Http.GetFromJsonAsync<Rate>("https://v6.exchangerate-api.com/v6/e0cd659d834c4d43ace77844/latest/"+ CurrencyCode);
feedback = "OK";
StateHasChanged(); //refresh screen
}
catch (Exception ex)
{
feedback = "Unrecognizable Country Currency Code ";
StateHasChanged();
}
}
private void TheButtonCleared()
{
//reset values
feedback = "";
CurrencyCode = "";
}
}
HTML Section
@page "/exchangepage"
@using ExchangeRatesAPI.Models
@inject HttpClient Http
<h3>Welcome to the Exchange Page</h3>
<div class="card m-3">
<h4 class="card-header">Exchange Rates</h4>
<div class="card-body">
<div class="form-group row">
<div class="form-group col-4">
<label style="color:blue">Enter Base Country Monetary Code</label>
<input @bind="@CurrencyCode" class="form-control" />
</div>
<div class="form-group col-4">
<label style="color:blue">Country</label>
<select class="form-select" @bind="@CurrencyCode">
<option value="">Pick Country from List</option>
@foreach (var item in countries)
{
<option value="@item.CurrencyCode">@item.CurrencyName - @item.CurrencyCode</option>
}
</select>
</div>
</div>
</div>
<div class="form-group">
<button class="btn btn-primary" @onclick="TheButtonClicked">Display Rates</button>
<button class="btn btn-primary" @onclick="TheButtonCleared">Clear</button>
</div>
</div>
@if (feedback!="OK")
{
@feedback
}
@if (rates == null)
{
<p><em>Loading...</em></p>
}
else if (feedback=="OK")
{
<h3> 1 @rates.base_code is worth as of @rates.time_last_update_utc.Substring(0,16):</h3>
<table table class="table table-bordered table-striped table-sm">
<thead>
<tr>
<th>Currency</th>
<th>Rate</th>
</tr>
</thead>
<tbody>
@foreach (var rate in rates.conversion_rates)
{
<tr>
<td>@rate.Key</td>
<td>@rate.Value</td>
</tr>
}
</tbody>
</table>
}
ExchangeRatesAPIupdatedOrderBy
First key update is the use of a more detailed and comprehensive list of Countries
[
{
"Country": "New Zealand",
"CountryCode": "NZ",
"Currency": "New Zealand Dollars",
"Code": "NZD"
},
{
"Country": "Cook Islands",
"CountryCode": "CK",
"Currency": "New Zealand Dollars",
"Code": "NZD"
},
Now we must update our class for countries to take into consideration these new components
public class NewCountry
{
public string Country { get; set; }
public string CountryCode { get; set; }
public string Currency { get; set; }
public string Code { get; set; }
//https://tableconvert.com/csv-to-json?data=https://gist.githubusercontent.com/HarishChaudhari/4680482/raw/b61a5bdf5f3d5c69399f9d9e592c4896fd0dc53c/country-code-to-currency-code-mapping.csv
}
Second key update is now a more comprehensive Dropdown List based on this NewCountry class.
In the code section
//Updated List used for DropDown component in UI
List<NewCountry> newcountries = new List<NewCountry>();
protected override async Task OnInitializedAsync()
{
//This newer list has more countries
newcountries = await Http.GetFromJsonAsync<List<NewCountry>>("sample-data/newcurrencies.json");
}
In the HTML section
<select class="form-select" @bind="@CurrencyCode">
<option value="">Pick Country from List</option>
@foreach(var item in newcountries.OrderBy(c =>c.Country))
{
<option value="@item.Code">@item.Country - @item.Currency - @item.Code</option>
}
</select>
currencies.csv
currencies.json
In this Lecture we will
Learn that Drag and Drop has become a popular interface solution in modern applications. It's common to find it in productivity tools like Trello and JIRA. These are more formally known as Project Management Tools or Project Trackers. They follow the Kanban style of list making.
Give an overview of the simple Project Tracker we will build over the next three lectures. It will have three zones, Requests, In Progress and Done. The application will allow you to enter new projects and give you the ability to drag then to the other zones as the projects progress to completion. The individual projects (tasks) will be date/time stamped and there will be trash zone to delete projects.
Learn about the concept of Attribute Splatting which will be implemented in our Project Tracker app
When a child component has many parameters, it can be tedious to assign each of the values in HTML. To avoid having to do that we can use attribute splatting.
With attribute splatting the attributes are captured in a dictionary and then passed to the component as one unit. One attribute is added per dictionary entry.
We reference the dictionary using the @attributes directive.
Example 1 (Basic Attribute Splatting ... AttributeSplatting1)
In the Shared folder Create a component (non-routable) called Button.razor . It has quite a few parameters
<button class="@Class" disabled="@Disabled" title="@Title" onclick="@ClickEvent">
@ChildContent
</button>
@code {
[Parameter]
public string Class { get; set; }
[Parameter]
public bool Disabled { get; set; }
[Parameter]
public string Title { get; set; }
//Here we use EventCallback(s) as a way to deal with nested components
//A common scenario is where a nested component executes a parent components
//method when a child component event occurs.
//An onclick event occuring in the child component is a common use case.
[Parameter]
public EventCallback ClickEvent{ get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
}
Now in the Pages folder create a component (routable) called SplatPage.razor
@page "/splatpage"
<h3>Attribute Splatting Demo</h3>
@* No Attribute Splatting *@
<Button Class="btn btn-danger"
Disabled="false"
Title="Here is a button"
ClickEvent="OnClickHandler">
<ChildContent>
<b>Submit</b>
</ChildContent>
</Button>
@code {
private void OnClickHandler()
{
}
}
By using attribute splatting we can simplify the HTML markup to this
@* Attribute Splatting *@
<Button @attributes="InputAttributes" ClickEvent="OnClickHandler">
<ChildContent>
<b>Submit</b>
</ChildContent>
</Button>
@code {
@* With attribute splatting the attributes are captured in a dictionary
and then passed to the component as a unit .One attribute is added per dictionary entry . We must use type object as second paramater because we have a boolean value for Disabled type object is the base class of all .NET classes and thus supports all classes*@
public Dictionary<string, object> InputAttributes { get; set; } =
new Dictionary<string, object>()
{
{"Class","btn btn-danger"},
{"Disabled",false},
{"Title","This is a button"}
};
//Alternate Dictionary declaration
//Dictionary<string, object> newInputAttributes = new Dictionary<string, object>();
//protected override void OnInitialized()
//{
// newInputAttributes.Add("Class", "btn btn-danger");
// newInputAttributes.Add("Disabled", false);
// newInputAttributes.Add("Title", "This is a button");
//}
private void OnClickHandler()
{
}
}
... but the real power of attribute splatting is realized when it is combined with arbitrary parameters.
Example 2 (Arbitrary Parameters ... AttributeSplatting2)
In the preceding example we used explicitly defined parameters to assign the button attributes. A much more efficient way of assigning values to attributes is to use Arbitrary parameters.
An Arbitrary parameter is a parameter that is not explicitly defined by the component. The Parameter attribute has a CaptureUnmatchedValues property that is used to capture any arbitrary parameters.
... so now update the Button.razer page to this
<button @attributes="InputAttributes">
@ChildContent
</button>
@code {
@* Used in Arbitrary Parameters Implementation *@
[Parameter(CaptureUnmatchedValues=true)]
public Dictionary<string,object>InputAttributes { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
}
... and the HTML/code in the SplatPage.razor looks like this
@page "/splatpage"
<h3>Attribute Splatting Demo</h3>
<Button @attributes="InputAttributes"
@onclick="ButtonClicked">
<ChildContent>
<b>Submit</b>
</ChildContent>
</Button>
@code {
@* This is the definition of InputAttributes used by the new version of the markup *@
public Dictionary<string, object> InputAttributes { get; set; } =
new Dictionary<string, object>()
{
{"class","btn btn-danger"},
{"title","This is a newer button"},
{"name","btnSubmit"},
{"type","button"}
};
private void OnClickHandler()
{
}
}
Get started with the App (ProjectTracker1)
The Blazor WebAssembly application that we are going to build will have 3 dropzones to indicate the progress of tasks/projects. We will be able to drag and drop tasks between the dropzones and add additional tasks.
Create a new WebAssembly App called ProjectTracker
Adding the classes
create a folder called Models
create a class called ProjectStatus
public enum ProjectStatus
{
Requested,
InProgress,
Done
}
create a class called ProjectItem
public class ProjectItem
{
public string ProjectName { get; set; }
public ProjectStatus Status { get; set; }
}
.... In our next lecture we create the Dropzone component (not the actual board but the component which will be implemented by the board)
Supplementary Demos
Lecture150MoreBlazorAttributeSplatting
Simple review example of Attribute Splatting and Arbitrary Parameters that progresses through several implementations
Calling a non-routable component with no parameters <Home/>
Calls the Home component and passes the Title only <Home Title="Welcome'/> ... image is harded in Home component
Implementing Arbitrary parameters
Home.razor
<div style="text-align:center">
<img @attributes="AdditionalAttributes" class="img-fluid" />
</div>
@code {
[Parameter]
public string Title { get; set; }
[Parameter(CaptureUnmatchedValues =true)]
public Dictionary<string,object>AdditionalAttributes{ get; set; }
}
Index.razor
@page "/"
<Home Title="Welcome to the NBA Jersey Page" @attributes="AdditionalAttributes" />
@code {
public Dictionary<string, object> AdditionalAttributes { get; set; } = new Dictionary<string, object>
{
{"src","images/siakam.jpg"},
{"alt", "Siakam"}
};
}
Lecture150MoreEventCallbackNET6
In this simple application we review and extend our knowledge of
Parameters
Render Fragments
EventCallbacks
... Look through ChildComponent.razor in Shared folder
... Look through ParentComponent.razor in Pages folder
ChildComponent.razor
<div>
<div class="alert alert-info">@Title</div>
<div class="alert alert-success">
@if (ChildContent != null)
{
<span>@ChildContent</span>
}
else
{
<span>Empty Render Fragment</span>
}
</div>
<button class="btn btn-danger" @onclick="OnButtonClick">Button to Click</button>
<button class="btn btn-secondary" @onclick="OnButtonClear">Clear</button>
</div>
@code {
[Parameter]
public string? Title { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter]
public EventCallback OnButtonClick { get; set; }
[Parameter]
public EventCallback OnButtonClear{ get; set; }
}
ParentComponent.razor
@page "/parentcomponent"
<h3>Parent Child Relation </h3>
<ChildComponent OnButtonClick="ShowMessage" OnButtonClear="ClearMessage" Title="Title passed from Parent">
<ChildContent>Render Fragment from Parent</ChildContent>
</ChildComponent>
<p><b>@messageText</b></p>
@*This component call does not reference the OnButtonClick or OnButtonClear parameters
... The buttons will appear but they will not be active
This component call also does not implement a Render Fragment
*@
<ChildComponent Title="Title passed from Parent ... No Render Fragment"/>
@code {
public string messageText = "";
private void ShowMessage()
{
messageText = "Button clicked from Child Component";
}
private void ClearMessage()
{
messageText = "";
}
}
In this Lecture we will
Create the Dropzone component to be incorporated in the main Project board in the next lecture and it's associated style sheet. (ProjectTracker2)
In the Shared folder create a non-routable Razor component called Dropzone.razor
First we remove the h3 element at the top and add a @using directive
@using ProjectTracker.Models
Next we move down to the code block
@code {
[Parameter]
public List<ProjectItem> ProjectItems { get; set; }
[Parameter]
public ProjectStatus Status { get; set;}
//Here we use EventCallback(s) as a way to deal with nested components
//A common scenario is where a nested component executes a parent components
//method when a child component event occurs.
//An onclick event occuring in the child component is a common use case.
[Parameter]
public EventCallback<ProjectStatus> OnDrop { get; set; }
[Parameter]
public EventCallback<ProjectItem> OnStartDrag{ get; set;}
//Here we are invoking methods that will be/are on the Project Board component page
private void OnDropHandler()
{
OnDrop.InvokeAsync(Status);
}
private void OnDragStartHandler(ProjectItem task)
{
OnStartDrag.InvokeAsync(task);
}
}
Now we go back up to our HTML markup section
We now define the contents of a generic Dropzone which will then be implemented/called 3 times in our Tracker Board (to be created next lecture)
We are allowing tasks to be dropped into the element by preventing the default value of the ondragover event. The OnDropHandler method is called when an element is dropped into the Dropzone. Then we loop through all of the ProjectItems of the indicated Status.
<div class="projectstatus">
<h2>@Status.ToString()</h2>
@* Here we are labeling the dropzone (1 of 3) by it's status
and allowing elements to be dropped into it by preventing the
default value of the ondragover event. (The user is usually not
allowed to drop into another element)
The OnDropHandler method is called when an element is dropped into
the dropzone
... and finally
We then loop through all the ProjectItems of the indicated Status' *@
<div class="dropzone"
ondragover="event.preventDefault();"
@ondrop="OnDropHandler">
@* q.Status is the status of the current object in the ProjectItems list
... and Status is the Dropzone container Status (a parameter declared in the code section)
This will be called in the main Project Board like this
<Dropzone Status="ProjectStatus.Requested ProjectItems="ProjItems" ... *@
@foreach(var item in ProjectItems
.Where(q=>q.Status == Status) )
{
@*Here we make the div element draggable by setting
the draggable element to true
The OnDragStartHandler method is called when the
element is dragged*@
<div class="draggable"
draggable="true"
@ondragstart= "@(() => OnDragStartHandler(item))">
@item.ProjectName
<span class="badge badge-secondary">
@item.Status
</span>
</div>
}
</div>
</div>
Alternate solution ... no lambda expression
@foreach(var item in ProjectItems)
@*.Where(q=>q.Status == Status))*@
{
@if(item.Status==Status)
{
<div class="draggable"
draggable="true"
@ondragstart= "@(() => OnDragStartHandler(item))">
@item.ProjectName
<span class="badge badge-secondary">
@item.Status
</span>
</div>
}
}
... Create a style sheet specifically for the Dropzone component using CSS isolation (DropzoneStyleSheet.txt)
Name the style sheet Dropzone.razor.css ... this will localize the style sheet specifically to the Dropzone.razor component .
.draggable {
margin-bottom: 10px;
padding:10px 25px;
border:1px solid #424d5c;
cursor:grab;
background:#ff6a00;
color:#ffffff;
border-radius:5px;
width:16rem;
}
.draggable:active{
cursor:grabbing;
}
.dropzone {
padding:.75rem;
border:2px solid black;
min-height:20rem;
}
.projectstatus {
min-width:20rem;
padding-right:2rem;
}
... In our last lecture we finally create the main Project Tracker board and incorporate an Add Task capability.
In this Lecture we will
Complete our Project Tracker app by adding our main display board and incorporate an Add Task capability
Create the Project Board (ProjectTracker3)
In the Pages folder create a razor component called TrackerPage.razor
Add the @page and @using directives
@page "/trackerpage"
@using ProjectTracker.Models
<h3>Project Tracker Board</h3>
Next we move down to the code block
@code {
public ProjectItem CurrentItem;
List<ProjectItem> ProjItems = new List<ProjectItem>();
//Initialize the ProjectItems object with three projects/tasks
protected override void OnInitialized()
{
ProjItems.Add(new ProjectItem
{
ProjectName = "Fix Car",
Status = ProjectStatus.Requested
});
ProjItems.Add(new ProjectItem
{
ProjectName = "Paint House",
Status = ProjectStatus.InProgress
});
ProjItems.Add(new ProjectItem
{
ProjectName = "Mow Lawn",
Status = ProjectStatus.Done
});
}
//Set the value of CurrentItem to the item that is currently being dragged
//We will use this value when the item is dropped
//This method was invoked in the DropZone.razor component
//private void OnDragStartHandler(ProjectItem task)
//{
// OnStartDrag.InvokeAsync(task);
//}
// ... Where task was passed in a value from
//@ondragstart= "@(() => OnDragStartHandler(item))"
private void OnStartDrag(ProjectItem item)
{
CurrentItem = item;
}
//Here we set the status of the CurrentItem to the status associated with the
//dropzone that it is dropped into
//This method was invoked in the DropZone.razor component
//private void OnDropHandler()
//{
// OnDrop.InvokeAsync(Status);
//}
private void OnDrop (ProjectStatus status)
{
CurrentItem.Status = status;
}
}
Now we go back up to our HTML markup section
We need to add three dropzones to create our Project Tracker board, one dropzone for each of the three status types for a task/project.
@* Here we add the 3 dropzones to our Project Tracker Board
Most of the work for the display happens in our DropZone component
which is being fed all the necessary parameters *@
<div class="row p-2">
<DropZone Status="ProjectStatus.Requested"
ProjectItems="ProjItems"
OnDrop="OnDrop"
OnStartDrag="OnStartDrag" />
<DropZone Status="ProjectStatus.InProgress"
ProjectItems="ProjItems"
OnDrop="OnDrop"
OnStartDrag="OnStartDrag" />
<DropZone Status="ProjectStatus.Done"
ProjectItems="ProjItems"
OnDrop="OnDrop"
OnStartDrag="OnStartDrag" />
</div>
Add a link to the TrackerPage in the NavMenu and test out the app so far.
Add a New Task/Project component (ProjectTracker4) ... Utilizing Attribute Splatting and Arbitrary Parameters
We only have 3 pre-defined (hard coded) projects presently in the app. We need a way to add new projects via an input form control
First we will create a new Razor component called NewProject in the Shared folder
Let's start with the code section first
@code {
private string projName;
[Parameter]
public EventCallback<string> OnSubmit { get; set; }
//Here we now implement Attribute Splatting combined with
//Arbitrary Parameters
[Parameter(CaptureUnmatchedValues =true)]
public Dictionary<string,object> InputParameters { get; set; }
private async Task OnClickHandler()
{
if(!string.IsNullOrWhiteSpace(projName))
{
await OnSubmit.InvokeAsync(projName); //since we are using async Task
projName = null; //we must using await
}
}
}
Now we go back up to our HTML markup section and add our input control ( See NewProjectHTML.txt for full code )
<div class="row p-3" style="max-width:950px">
<div class="input-btn-group mb-3">
<label class="input-group-text" for="inputProject">
Project
</label>
<input type="text"
id="inputProject"
class="form-control"
@bind-value="@projName"
@attributes="InputParameters" />
<button type="button" class="btn btn-outline-secondary" @onclick="OnClickHandler">
Add Project
</button>
</div>
</div>
Finally we need to add a reference to this NewProject component in the main board page TrackerPage.razor.
First In the code section we make the following updates.
//Attribute Splatting combined with Arbitrary parameters
public Dictionary<string,object> InputAttributes = new Dictionary<string, object>()
{
{"maxlength",25},
{"placeholder", "enter new project"},
{"title","This textbox is used to enter your projects"}
};
//Recall ... this method is invoked in the NewProject.razor component
//await OnSubmit.InvokeAsync(projName);
private void AddTask(string projName)
{
var projItem = new ProjectItem()
{
//assign the new Project Name entered in the input
//to the class property ProjectName
//and default the Status property to Request
ProjectName = projName,
Status = ProjectStatus.Requested
};
ProjItems.Add(projItem);
}
Alternate AddTask methods
private void AddTask(string projName)
{
ProjItems.Add(new ProjectItem
{
ProjectName = projName,
Status = ProjectStatus.Requested
});
}
private void AddTask(string projName)
{
//Another Method to add a new project item
ProjectItem projItem = new ProjectItem();
projItem.ProjectName = projName;
projItem.Status = ProjectStatus.Requested;
ProjItems.Add(projItem);
}
... and then we add in the HTML markup we add
<NewProject OnSubmit "AddTask" @attributes ="InputAttributes"/>
Offer you several challenges and Supplementary Demos
Implement a Delete capability (via drag and drop of course) (ProjectTracker5Delete)
@* This dropzone will be used to delete a project item via drag and drop ...
A new enum element was added to the ProjectStatus model class called Delete
When an object is dropped here we excecute a NEW method called OnDropDelete *@
<DropZone Status="ProjectStatus.Delete"
ProjectItems="ProjItems"
OnDrop="OnDropDelete"
OnStartDrag="OnStartDrag" />
private void OnDropDelete (ProjectStatus status)
{
//A object was dropped into the Delete Dropzone ... remove it from the ProjItems List
//The item being dragged ... CurrentItem ... was stored when dragging began in the OnStartDrag method
ProjItems.Remove(CurrentItem);
}
Add a Trash Can icon to the Delete DropZone
<div class="projectstatus">
@if(Status.ToString()=="Delete")
{
<h2><span class="oi oi-trash"></span></h2>
}
else
{
<h2>@Status.ToString()</h2>
}
Implement a Time/Date stamp and a Count of the current contents of each of the 3 major drop zones (ProjectTracker6TimeAndCount)
Time/Date Stamp
First we add a new property to the ProjectItem class
public DateTime LastUpdated { get; set; }
In the TrackerPage
private void OnStartDrag(ProjectItem item)
{
CurrentItem = item;
CurrentItem.LastUpdated = DateTime.Now; //The instant we start dragging mark the time
}
private void AddTask(string projName)
{
var projItem = new ProjectItem()
{
ProjectName = projName,
Status = ProjectStatus.Requested,
LastUpdated=DateTime.Now //a new task/project uses
//the default time of NOW
};
ProjItems.Add(projItem);
}
In the DropZone page
@* Display the DateTime below the Status *@
<span class="badge badge-secondary">
@item.LastUpdated
</span>
Count (all updates in the DropZone.razor page)
@* Counting the number of tasks/projects in each Dropzone
Here we initalize our counter before the foreach loop is executed*@
@{
var c=0;
}
@foreach(var item in ProjectItems
.Where(q=>q.Status == Status))
{
c=c+1;
<div class="draggable"
draggable="true"
@ondragstart= "@(() => OnDragStartHandler(item))">
@item.ProjectName
<span class="badge badge-secondary">
@item.Status
</span>
<br />
@* Display the DateTime below the Status *@
<span class="badge badge-secondary">
@item.LastUpdated
</span>
</div>
}
@* We don't want to display a counter in the Delete Zone' *@
@{
if (Status.ToString() != "Delete")
{
var text = "(" + c.ToString() + ")";
@text
}
}
Update the application so that it connects to a database which will store the current projects/tasks. This will obviously make the application much more useful and realistic. (ProjectTrackerDB) ... Look through Lectures 143-145 for ideas.
In this Lecture we will
Revisit and Review the components that make up a typical Authentication System
UI (Register/Login)
Functionality (Authentication/Authorization)
Data Storage
Review the difference between Authentication and Authorization
Authentication is the process of determining if someone is who they claim to be
most common way is a username and password check
another example of authentication is using your pin code with a credit card
Authorization is the process of checking if someone has the rights to access a resource (what the user can and cannot do)
Authorization occurs after an identity has been established via authentication and determines what parts of a system you can access. For example, if you have administrator rights on a system you can access everything. But if you're a standard user, you may only be able to access specific screens.
Create a new Blazor WebAssembly Core Hosted .NET 6 application which implements authentication and authorization. (See Lecture 178 for .NET 8 Version)
Intro (WasmAuthenication1Intro)
We can't simply use a basic WebAssembly app here because of the data storage requirements for authentication. WebAssembly restricts apps from accessing a database. However, there is another way to perform database operations. We can create an ASP.NET Web API controller to wrap your database connection via the Core Hosted Site option.
We will work with the Default Authentication (choose Individual Accounts option)
Modify the connection string database name to "WasmAuthDb" in appsettings.json and then update-database in the Package Manager Console
inspect the newly created tables
Test out Authentication and Register a new account ( this user will have a role assigned to him/her in the second iteration of our code)
There are several components that provide the authentication mechanism in the Blazor WebAssembly application.
The App.razor component – is a central part of the BlazorWebAssembly authentication:
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@if (context.User.Identity?.IsAuthenticated != true)
{
<RedirectToLogin />
}
else
{
<p role="alert">You are not authorized to access this resource.</p>
}
</NotAuthorized>
</AuthorizeRouteView>
Basically, in this component, the AuthorizeRouteView component checks if the current user is authenticated. If that’s not the case, the user is redirected to the Login page with the RedirectToLogin component.
Start the app , don't login and and click Home or Counter ... no issues. Click FetchData and you are directed to login.
By default access to FetchData Page for users not logged in is blocked
@page "/fetchdata"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@attribute [Authorize]
Remove <RedirectToLogin/> and instead use
<p role="alert">Please login</p>
... follow the path ... The RedirectToLogin component is found in the Shared folder and it forces a Navigation to ... the Authentication component located in the Pages folder ... which lastly calls the RemoteAuthenticatorView which comes from the package Microsoft.AspNetCore.Components.WebAssembly.Authentication
Re-run the application and note the "Register" and "Log in"
These links are displayed in the MainLayout.razor page
<div class="top-row px-4 auth">
<LoginDisplay />
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>
The LoginDisplay component is located inside the Shared folder
Here, we can see the Authorized component, which will be rendered if a user is authorized, and NotAuthorized component for unauthorized users. Inside that component, we can find two links with the href attributes pointing to the Authentication component with different actions.
Customizing components ... create a new CustomLoggedOut.razor component:
When we log out we see the logged out page with a simple message. We are going to add a custom behaviour to it.
First we create a new CustomLoggedOut.razor component in the Shared folder
<h3>You are successfully logged out</h3>
<p>
You can always <a href="/authentication/login">Log in</a>again
</p>
Then all we have to do is modify the Authentication.razor component.
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action">
<LogOutSucceeded>
<CustomLoggedOut/>
</LogOutSucceeded>
</RemoteAuthenticatorView>
@code{
[Parameter] public string? Action { get; set; }
}
The RemoteAuthenticatorView component provides us with different components that we can modify to change the UI in certain authentication stages. Here we modified the LogOutSucceeded component by calling the CustomLoggedOut component.
In this Lecture we will
Continue our look at Authentication in Blazor WebAssembly and focus on Role Based Authentication for an additional level of security (WasmAuthenication2Roles)
First we have to support Roles for ASP.NET Core Identity . To do that we need to modify the configuration in Program.cs (Server Project Section) to work with roles
builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
note: add
using Microsoft.AspNetCore.Identity;
Next with need to add Roles to the Claims. We again need to modify the Program.cs class (See ProgramcsUpdate.txt)
//Necessary Addition for Role Based Authentication
builder.Services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(opt =>
{
opt.IdentityResources["openid"].UserClaims.Add("role");
opt.ApiResources.Single().UserClaims.Add("role");
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
Next we are going to load up the Blazor Utility App IdentityManager into Visual Studio. This application will allow us to assign roles with a nice UI instead of deep diving into the LocalDB Tables
Before we can use it we must make two modifications
First we must change Startup reference from .UseSqlite to .UseSqlServer
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
Second in appsettings.json need to change connection string to match one used in current app
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=WasmAuth;Trusted_Connection=True;MultipleActiveResultSets=true"
}
Now execute app and let it connect to your local db within the project
Add two roles ... Admin and Visitor
Assign the Admin role to a user
Assign the Visitor role to a user
Update .NET 7 +
Use the IdentityManager Utility Application (CarlFranklin Version)
Remember to update the appsetting.json Database file to match the Database name in our Application and make the Server App the startup Project.
Now that we have roles configured and placed in the database, we can use that to protect our pages and actions
Let's modify the FetchData component to allow the Admin role only
@page "/fetchdata"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using WasmAuthentication.Shared
@attribute [Authorize (Roles ="Admin")]
@inject HttpClient Http
Start the app and inspect the results
Finally, lets not show the Fetch Data link for the non-admin users. For this we need to modify the NavMenu.razor file.
<AuthorizeView Roles="Admin">
<div class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</div>
</AuthorizeView>
In this Lecture we will
Review the difference between a flat file DB and a relational DB
Sketch out our two table relational database app (Quick demo of final full CRUD version)
Category table (Id, Genre,Description) ... static table
Song table ( Id, SongTitle, Artist, Year, CategoryId ) ... dynamic table
Where CategoryId will be our link to the Category table from the Song table ... so that we can display Genre and Description
Get started creating the App
Create a new WebAssembly App ... make sure to check ASP.NET Core Hosted (SongWasmRelDB1 ) ... Recall Lecture 143 (Vaccination Registration App)
Let's start by clearing out a number of the components of the default template solution
Delete all the components in the Client.Pages folder except for the Index page.
Delete the SurveyPrompt.razor page in the Client.Shared folder
In the NavMenu change FetchData and Counter to Categories (catpage) and Songs (songpage)
From the Server.Controllers folder remove the WeatherForecastController.cs file
From the Shared Project delete the WeatherForecast.cs file
Rebuild the solution.
Add the classes, the API Controllers and Create the SQL Server database for the Category table
First we add the Category class to the Shared Project
public class Category
{
public int Id { get; set; }
[Required (ErrorMessage="Genre is required")]
public string? Genre { get; set; }
public string? Description {get; set;}
}
Next we add the API controller
Right click the Controllers folder in the SongWasmRedDB.Server project and select Add, Controller option and then choose API Controller with actions, using Entity Framework.
In the newly created controller update the route to [Route("[controller]")]
Setting up the SQL Server
We need to create a new sql database and add a table to it which will contain our Category list.
First we modify the connection string in the appsettings.json file
"ConnectionStrings": {
"ToDoListAPIServerContext": "Server=(localdb)\\mssqllocaldb;Database=SongGenreDB;Trusted_Connection=True;MultipleActiveResultSets=true"
}
Now we go into the Tools menu ... NuGet Package Manager, Package Manager Console and execute
Add-Migration Init
Update-Database
The preceding commands used Entity Framework migrations to update the SQL Server ... let's go take a look at the DB
From the View menu select SQL Server Object Explorer
Click on localdb ... then Databases and you should see the GenreDB
Click the DB and then go inside the Tables folder
.. and finally click on dbo.Category to view the table definition
Right click dbo.Category and select the View Data option
Enter a few Categories(Genre,Description) ... don't enter anything into the ID field , it is automatically generated.
Time to make sure the app works so far ... start up the app
It will default to the Index page ... hopefully with no errors
... but we really want to make sure our Controller works so you will need to manually (for now) go up to the address bar and type Categories (name of controller)
It should return a simple JSON display of the current contents of the DB.
Next up ... we can now start working on our client project ... the UI
In this Lecture we will
Focus on the client project UI and create our Category List and Category Edit Pages for our Song Application (SongWasmRelDB2)
First let's go back to the index.razor page and add some images and a new welcome
<PageTitle>Songs</PageTitle>
<h3>Welcome To The Song Genre App</h3>
<img src="images/genres.png" height="300" width="350" />
<br />
Now lets focus on our main Category List Page
Create a routable razor component called CatPage
Update the top part of the markup
@page "/catpage"
@using SongWasmRelDB.Shared
@inject HttpClient Http
Next update the code section
@code {
IList<Category>? categories;
protected override async Task OnInitializedAsync()
{
categories = await Http.GetFromJsonAsync<IList<Category>>("Categories");
}
}
Add the remaining HTML code ... note the Bootstrap shadow effect
<div class="row mt-4 shadow p-4 mb-4 bg-white">
<div class="col-6">
<h4 class="card-title text-primary">Genre List</h4>
</div>
<div class="col-4 offset-2">
<a href="cat" class="btn btn-info form-control">Add New Genre</a>
</div>
</div>
@if (categories == null)
{
<div class="text-center">
<p><em>Loading ...</em></p>
<img src="images/loading.gif" />
</div>
}
else if (categories.Count == 0)
{
<div>None Found</div>
}
else
{
<table class="table table-bordered table-hover ">
<thead>
<tr class="table table-dark">
<th>Action</th>
<th>Genre</th>
<th>Description</th>
</tr>
</thead>
<tbody>
@foreach (var item in categories)
{
<tr>
<td>
<a href="/cat/@item.Id">Edit</a>
</td>
<td>@item.Genre</td>
<td>@item.Description</td>
</tr>
}
</tbody>
</table>
}
Test out our progress so far ...
note animated gif for pre-loading
note cat razor page component doesn't exist yet we create that page next
Finally lets create our Editing page
Create a routable razor component called CatEdit
We are going to have this Edit page handle both Record Edits and New Record creation by implementing Route Parameters ... add this to the top of the HTML section
@page "/cat"
@page "/cat/{id:int}"
@using SongWasmRelDB.Shared
@inject HttpClient Http
@inject NavigationManager Nav
Now lets declare id parameter and implement the logic to determine whether we are editing a current record or creating a new one.
@code {
[Parameter]
public int id { get; set; }
private bool ready;
private string? error;
private Category? category;
protected override async Task OnInitializedAsync()
{
if (id == 0)
{
category = new Category();
}
else
{
category = await Http.GetFromJsonAsync<Category>($"Categories/{id}");
}
ready = true;
}
}
Let's initially add a little bit of HTML code so we can test out what we have so far.
@if(id==0)
{
<h4>Creating New Genre</h4>
}
else
{
<h4>Edit </h4>
}
@if (!ready)
{
<p><em>Loading ... </em></p>
}
else
{
}
Now lets complete the HTML section for our Edit Page and the necessary methods in the code section
First the HTML ... after the else ... using EditForm (see CodeSnippet)
<div class="card m-3">
<h4 class="card-header">Genre Update Form</h4>
<div class="card-body">
<EditForm Model="@category" OnValidSubmit="@HandleValidSubmit" @onreset="HandleReset">
<DataAnnotationsValidator />
<div class="form-group row">
<div class="form-group col-5">
<label>Genre</label>
<InputText @bind-Value="category.Genre" class="form-control" />
<ValidationMessage For="@(() => category.Genre)" />
</div>
<div class="form-group col-7">
<label>Description</label>
<InputTextArea @bind-Value="category.Description" class="form-control" rows="4"></InputTextArea>
<ValidationMessage For="()=>category.Description"></ValidationMessage>
</div>
</div>
<div class="card-footer">
<div class="text-center">
<button type="submit" class="btn btn-primary mr-1">Save</button>
<button class="btn btn-outline-danger btn-sm" title="Delete task" @onclick="@DeleteCategory">
<span class="oi oi-trash"></span>
</button>
<button type="reset" class="btn btn-secondary">Cancel</button>
</div>
</div>
</EditForm>
</div>
</div>
Then the code
private async Task HandleValidSubmit()
{
HttpResponseMessage response;
if (category.Id == 0)
{
response = await Http.PostAsJsonAsync("Categories", category);
}
else
{
string requestUri = $"Categories/{category.Id}";
response = await Http.PutAsJsonAsync(requestUri, category);
}
if (response.IsSuccessStatusCode)
{
Nav.NavigateTo("catpage");
}
else
{
error = response.ReasonPhrase;
}
}
private void HandleReset()
{
Nav.NavigateTo("catpage");
}
private async Task DeleteCategory()
{
string requestUri = $"Categories/{category.Id}";
var response = await Http.DeleteAsync(requestUri);
if (response.IsSuccessStatusCode)
{
Nav.NavigateTo("catpage");
}
else
{
error = response.ReasonPhrase;
}
}
Test out our progress ... next up entering actual Artists , Songs , Genre, and Year
In this Lecture we will
Focus on how to connect (make a relationship) a second table (Song) to an already existing DB table (Category) (SongWasmRelDB3)
First we create a new class called Song in the Shared project
public class Song
{
public int Id { get; set; }
[Required]
public string? SongTitle { get; set; }
[Required]
public string? Artist { get; set; }
public string? Year { get; set; }
[Range(1, int.MaxValue, ErrorMessage = "Please select a Category")]
public int CategoryId { get; set; }
[ForeignKey("CategoryId")] //Once migration is completed you can comment this line out
public Category? Category { get; set; }
// Note that Category? Category
// is called a Category navigation property. It does not get directly saved as a column in the database.
// Instead, Entity Framework Core uses the CategoryId property as a foreign key column
// in the Song table to establish the relationship.
// It allows you to easily access the full Category object from a Song instance, not just the CategoryId.
// For example, you can write: song.Category.Genre to get the genre name directly from a song.
///... Now Create a new API controller based on this class and then
//Remember to perform a 2nd migration
//so that the relation between the two tables
//is recognized
//Declaring a ForeignKey as CategoryId in the Song Table is crucial
//It will act as our connection with the Id of the Category table
//Add-Migration Relational
//Update-Database
//... you should now see two tables in the GenreDB
}
Next we add the API controller
Right click the Controllers folder in the SongWasmRelDB.Server project and select Add, Controller option and then choose API Controller with actions, using Entity Framework. This time pick the Song class.
In the newly created controller update the route to [Route("[controller]")]
Now we go into the Tools menu ... NuGet Package Manager, Package Manager Console and execute
Add-Migration Relational
Update-Database
Lot's of really important things happened here ... note the following
In the new Migration file (Migration folder) the foreign field has been recognized
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Song",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
SongTitle = table.Column<string>(type: "nvarchar(max)", nullable: false),
Artist = table.Column<string>(type: "nvarchar(max)", nullable: false),
Year = table.Column<string>(type: "nvarchar(max)", nullable: true),
CategoryId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Song", x => x.Id);
table.ForeignKey(
name: "FK_Song_Category_CategoryId",
column: x => x.CategoryId,
principalTable: "Category",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Song_CategoryId",
table: "Song",
column: "CategoryId");
}
Check out the results in SQL Server Object Explorer and notice two tables in the SongGenreDB
Notice that the Song property Category is not part of the raw data since it is an Object of the another class (Category) not a primitive type ... it will be included via an update to the SongsController below
... so that it can be used to reference fields in the Category table from the Song table when we display them in the SongPage.razor which we create below. @item.Category.Genre
... And finally before we can move on and create our UIs for the Song Page we must update the newly created SongsController to recognize the link between the Song table and Category table.
// GET: api/Songs
[HttpGet]
public async Task<ActionResult<IEnumerable<Song>>> GetSong()
{
if (_context.Song == null)
{
return NotFound();
}
return await _context.Song.Include(u => u.Category).ToListAsync();
//return await _context.Song.ToListAsync();
//Need to add Include(u=>u.Category) to have Category fields populate
//with the Song db table fields
}
// GET: api/Songs/5
[HttpGet("{id}")]
public async Task<ActionResult<Song>> GetSong(int id)
{
if (_context.Song == null)
{
return NotFound();
}
var song = await _context.Song.Include(u => u.Category).FirstOrDefaultAsync(u => u.Id == id);
//var song = await _context.Song.FindAsync(id);
//Again, note the addition of Include(u=>u.Category)
//so that we grab all the fields for a specific song based on the id of the Song db table and
//drag in the Category name and description from the Category db table
//... we also use .FirstOrDefaultAsync(u=>u.Id==id)
if (song == null)
{
return NotFound();
}
return song;
}
Now we can create our SongPage and SongEdit just like we did for Category
First create a new razor component called SongPage
Let's add our heading information first
@page "/songpage"
@using SongWasmRelDB.Shared
@inject HttpClient Http
Next a little bit of code
@code {
IList<Song>? songs;
protected override async Task OnInitializedAsync()
{
songs = await Http.GetFromJsonAsync<IList<Song>>("Songs");
}
}
Now we can add the HTML code to display the contents of the Song table + Genre and Description from the Category table (see CodeSnippetSongPage)
@foreach (var item in songs)
{
<tr>
<td>
<a href="/song/@item.Id">Edit</a>
</td>
<td>@item.Artist</td>
<td>@item.SongTitle</td>
<td>@item.Year</td>
<td>@item.Category.Genre</td>
<td>@item.Category.Description</td>
</tr>
}
... not quite done ... last step is to add an Edit component so we can add Songs to our DB and edit them ... that's next
In this Lecture we will
Create the SongEdit page (SongWasmRelDB4) so that we can add new Songs and edit them
First we create a new razor component called SongEdit
Now just as we did with CatEdit we want to maximize the use of this component so we will again implement Route Parameters
@page "/song"
@page "/song/{id:int}"
@using SongWasmRelDB.Shared
@inject HttpClient Http
@inject NavigationManager Nav
Now lets go into the code section and declare the id Parameter , and either create a new Song object or load back in an existing record.
[Parameter]
public int id { get; set; }
private bool ready;
private string? error;
private Song? song;
IList<Category>? categories;
protected override async Task OnInitializedAsync()
{
categories = await Http.GetFromJsonAsync<IList<Category>>("Categories");
if (id == 0)
{
song = new Song();
}
else
{
song = await Http.GetFromJsonAsync<Song>($"Songs/{id}");
}
ready = true;
}
Note: You may wonder why we are declaring a list of objects of type Category ... we are going to use this list in a DropDown List (InputSelect) to choose one of the possible Genres stored in the Category table.
Now we create our UI using HTML code similiar to our Category Edit Page implementing EditForm (see CodeSnippetSongEdit for full HTML code)
<div class="form-group col-5">
<label>Genre</label>
<InputSelect @bind-Value="song.CategoryId" class="form-select">
<option value="0" disabled="disabled" selected>Select Category</option>
@foreach (var cat in categories)
{
<option value="@cat.Id">@cat.Genre</option>
}
</InputSelect>
<ValidationMessage For="()=>song.CategoryId"></ValidationMessage>
</div>
Note how we display the Genre in our DropDown list but internally track the cat.Id and bind to song.Category.Id
The Power of Relational Databases vs Flat File Databases
If this app would have used a Flat File you would need to enter the entire Genre Description for each and every new entry (crazy and very inefficient) ... whereas with the Relational DB you simply pick the Genre from the Dropdown List (in the SongEdit page) and have the complete detailed description brought in from the associated Category Table when you display it in the SongPage ... as we saw in the last Lecture.
What if you needed to update a Description ... again with a Flat File you would need to go to each and every occurence of that particular Genre in the Database and update the Description ... whereas with the Relational DB you simply go into the Category Table (CatEdit page) , update the specific Genre Description once and this change would work it's way into each and every occurence of that specific Genre in the SongPage display seamlessly.
Lastly we need our code to handle a Valid Submit, Delete or Reset
private async Task HandleValidSubmit()
{
HttpResponseMessage response;
if (song.Id == 0)
{
response = await Http.PostAsJsonAsync("Songs", song);
}
else
{
string requestUri = $"Songs/{song.Id}";
response = await Http.PutAsJsonAsync(requestUri, song);
}
if (response.IsSuccessStatusCode)
{
Nav.NavigateTo("songpage");
}
else
{
error = response.ReasonPhrase;
}
}
private void HandleReset()
{
Nav.NavigateTo("songpage");
}
private async Task DeleteSong()
{
string requestUri = $"Songs/{song.Id}";
var response = await Http.DeleteAsync(requestUri);
if (response.IsSuccessStatusCode)
{
Nav.NavigateTo("songpage");
}
else
{
error = response.ReasonPhrase;
}
}
Make some final tweaks and add some enhancements.
SongWasmRelDB5 ... demo only
Category Page
Filter by Genre
Sort by Genre
Song Page
Filter by Song Title
Sort by Song Title
SongWasmRelDB5withImages
Add a new property/field to the Song class which will contain an image
public string? ProfilePictureDataUrl { get; set; }
Need to perform a 3rd migration so that new property becomes part of the Song table in the database
In the SongEdit.razor page
<div class="form-group col-5">
<label>Image</label>
<InputFile OnChange="OnInputFileChange" class="form-contro"/>
</div>
<img src="@ProfilePicDataUrl" style="width:50px;height:50px;" />
//used to store image name(url) ... base 64 string image format
public string? ProfilePicDataUrl { get; set; }
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
//Recall: Lecture 132 Blazor Updates in .NET 5
var file = e.File;
var buffer = new byte[file.Size];
await file.OpenReadStream().ReadAsync(buffer);
ProfilePicDataUrl = $"data:image/png;base64,{Convert.ToBase64String(buffer)}";
song.ProfilePictureDataUrl = ProfilePicDataUrl;
}
In the SongPage.razor page
<td>
<a href="/song/@item.Id" class="btn btn-warning">Edit</a>
@*<a href="/song/@item.Id">Edit</a>*@
</td>
<td>@item.Artist</td>
<td><img src="@item.ProfilePictureDataUrl" style="width:50px;height:50px" /></td>
<td>@item.SongTitle</td>
<td>@item.Year</td>
<td>@item.Category.Genre</td>
<td>@item.Category.Description</td>
SongWasmRelDB5withImagesUpdated
Filter by multiple options ...
public bool IsVisible(Song s)
{
if (string.IsNullOrWhiteSpace(Filter))
return true;
if (s.SongTitle.Contains(Filter, StringComparison.OrdinalIgnoreCase))
return true;
if (s.Artist.Contains(Filter, StringComparison.OrdinalIgnoreCase))
return true;
if (s.Category.Genre.Contains(Filter, StringComparison.OrdinalIgnoreCase))
return true;
if (s.Year.Contains(Filter, StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
SongWasmRelDB6withImagesRowClickEdit
In the SongPage clicking on any row will also send you the SongEdit page.
@foreach (var item in songs)
{
if (!IsVisible(item))
continue;
<tr @onclick="@(() => HandleEdit(item.Id))">
<td>
<a href="/song/@item.Id" class="btn btn-warning">Edit</a>
@*<a href="/song/@item.Id">Edit</a>*@
</td>
<td>@item.Artist</td>
<td><img src="@item.ProfilePictureDataUrl" style="width:50px;height:50px" /></td>
<td>@item.SongTitle</td>
<td>@item.Year</td>
<td>@item.Category.Genre</td>
<td>@item.Category.Description</td>
</tr>
}
private async Task HandleEdit(int id)
{
string requestUri = "song/" + id.ToString();
Nav.NavigateTo(requestUri);
}
Your Challenge ... create a simple Relational DB application utilizing two tables to reinforce the ideas covered over the past 4 lectures
SampleChallengeSolution.rar (Employee Record Management
System)
Two table application Cities (Id, City,Description) Employees (Id,Name,Department,Gender,CityId,City City, ProfilePictureDataUrl)
SampleChallengeAppEmployeeManage
In EmpEdit.razor page note the use of InputSelect for Gender and Department (hard coded) and City via cities List (taken from DB)
EmployeesPage.razor
@foreach (var item in employees)
{
if (!IsVisible(item))
continue;
<tr>
<td>
<a href="/emp/@item.Id" class="btn btn-warning">Edit</a>
@*<a href="/song/@item.Id">Edit</a>*@
</td>
<td>@item.Name</td>
<td><img src="@item.ProfilePictureDataUrl" style="width:50px;height:50px" /></td>
<td>@item.Gender</td>
<td>@item.Department</td>
<td>@item.City.CityName</td>
<td>@item.City.CityDescription</td>
</tr>
}
SampleChallengeRadzen
Employee Record Management System implementing Radzen UI
Start without Debugging if execution issues.
ChalleRadUpdFooter (updated Radzen UI version)
Note the use of a FooterTemplate
<FooterTemplate>
Displaying : <b>@employeesGrid.View.Count()</b> of <b>@employees.Count()</b>
</FooterTemplate>
RadzenDataGrid<Employee>? employeesGrid;
Supplementary Demos
MovieWasmRelDB.rar
Two Table Application Category (Id, Genre, Description) Movie (Id, MovieTitle, Year, Image (string), CategoryId, Category Category)
MovieWasmRelDB9HelpDropDownFilter
In the MovieEdit.razor page note the use of the modal class which acts as a pop-up check if you "Wish to Delete"
@if (isOpening && id!=0)
{
<div class="modal fade show" id="myModal" style="display:block; background-color: rgba(10,10,10,.8);" aria-modal="true" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Are You Sure?</h4>
<button type="button" class="close" @onclick="closeModal">×</button> @*× is x symbol*@
</div>
<div class="modal-body">
<p>Press Delete to Remove this Entry or Cancel to continue Editing</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="closeModal">Cancel</button>
<button type="button" class="btn btn-danger" @onclick=DeleteMovie>Delete</button>
</div>
</div>
</div>
</div>
private async Task DeleteMovie()
{
isOpening = false;
var response = await Http.DeleteAsync("Movies/"+movie.Id);
if (response.IsSuccessStatusCode)
{
Nav.NavigateTo("moviepage");
}
else
{
error = response.ReasonPhrase;
}
}
private void closeModal()
{
isOpening = false;
}
In the MoviePage.razor page note the use of a Dropdown List to filter by Genre
<select class="form-control" @bind="Filter">
<option value="">Don't know all the Genre's ... Pick from List</option>
@foreach (var cat in categories.OrderBy(c=>c.Genre))
{
<option value="@cat.Genre">@cat.Genre</option> }
</select>
In the MoviePage.razor page note the row colours depending on the Year of the movie, row is clickable to send app to MovieEdit page.
<tr class="@((yr>1972)? "table-danger":"")" @onclick="@(() => HandleEdit(item.Id))">
MovieWasmRelDB10Radzen2
Radzen UI implemenation
Note in the CatPage.razor page the use of multiple icons for the Edit link
<tr>
<td>
<a href="/cat/@item.Id" class="btn btn-warning"><i class="oi oi-pencil"></i><i class="oi oi-delete"></i></a>
@*<a href="/cat/@item.Id">Edit</a>*@
</td>
<td>@item.Genre</td>
<td>@item.Description</td>
</tr>
ExpenseTracker
Two Table Application ExpenseType (Id, Type) Expense (Id, Date, vendor, Description, ExpenseTypeId, ExpenseType ExpenseType, Amount, Paid)
Key unique features ...
Looking up expenses between two dates via calendar entry
Code Section
//This is a NEW flag to recognize if we are filtering by Dates (using calendar input Date Picker)
//... if we are we will use a different Filtering Technique and using a slightly different UI display
//See HTML above
private bool dateFlag = false;
//used for the calendars inputs above
public DateTime From { get; set; } = DateTime.Now;
public DateTime To { get; set; } = DateTime.Now;
//***NEW UPDATED ***/
//stores filtered records for Expenses between From and To dates
IList<Expense> dateExpenses = new List<Expense>();
private void lookupDates(DateTime f, DateTime t)
{
dateExpenses.Clear();
dateTotal = 0;
dateCount = 0;
@foreach (var item in expenses)
{
if (item.Date >= f && item.Date <= t)
{
dateExpenses.Add(item);
//Here we are accumulating and counting the "filtered between date" results
//specifically for UNPAID expenses
if (!item.Paid)
{
dateTotal += (decimal)item.Amount;
dateCount++;
}
//Hey! what about the actual Total Amount and Count for the filtered between date results
//regardless of PAID or NOT PAID
//... No problem we just use
//@dateExpenses.Sum(p=>p.Amount)?.ToString("c") (Filtered @dateExpenses.Count)
}
}
dateFlag = true;//used in the HTML above to determine which UI display to use
}
HTML Section
<div class="input-group" style="float:left">
<label class="alert-info">From:</label><input class="form-control" type="date" @bind="From" />
<label class="alert-info">To:</label><input class="form-control" type="date" @bind="To" />
</div>
<button class="btn btn-success" @onclick="@(() => lookupDates(From,To))">Lookup Expenses Between ... </button>
Various Filtered totals
<tr>
<td class= "table-success">Total Expenses:</td>
<td></td>
<td></td>
<td></td>
<td class="table-secondary">@filterTotal.ToString("c") (Filtered @filterCount)</td>
<td class="table-primary">@expenses.Sum(p=>p.Amount)?.ToString("c") (Grand Total)</td>
<td class="table-danger">@Total.ToString("c") (Filtered Grand Total Not Paid @totalCount)</td>
</tr>
CascadingDropDown
In this Application, we are going to create a cascading dropdown list in Blazor WebAssembly (Core Hosted) using Entity Framework Core database first approach. We will create two dropdown lists — Country and City. Upon selecting the value from the Country dropdown, we will change the value of the City dropdown.
We will again follow the basic steps of implementing a Blazor WebAssembly DB application
This application will implement a Relational DB consisting of two tables Country and City
In the Shared Project we create two classes Country (Id, CountryName) and City (Id, CityName, CountryId, Country Country) that will be referenced to create our API Controller with Entity Framework
Add the API controller
Set up the SQL Server DB by first modifying the connection string (change DB name...CascadingDB) in the appsettings.json file
Add-Migration Init .... then Update-Database
Go into the SQL Server Object Explorer and enter some data into the Country table to kick start our DB
Next we focus on the Client Project UI
Create CountryPage.razor ... main screen to display Country Info
Create the CountryEdit.razor page ... where we add and edit Country info
Notice the use of Route parameters on the Edit page ... depending of whether the id=0 we either add a new Country or edit an existing Country
Create CityPage.razor ... main screen to display City Info
Create the CityEdit.razor page ... where we add and edit City info
Notice the use of Route parameters on the Edit page ... depending of whether the id=0 we either add a new City or edit an existing City
Finally we put the Cascading DropDown List coding on a separate razor component page (Cascading.razor)
In the code section we load in all the Country and City names into Lists
We declare a filteredCityList which will hold the cities for the chosen Country in the first DropDown List
Notice the key change to the first DropDown List ... we don't use bind-Value'
We use a series of 3 Value statements... the third one passing 'value' down to the OnValueChanged method
...after returning from the OnValueChanged method we now have a list filteredCityList which we will cycle through to create our 2nd DropDown List
... You cannot do bind-Value and @onchange at same time because InputSelect is a
component element, not HTML element which is why you cannot apply to it the @onchange
@bind-Value is a compiler directive instructing the compiler to produce code that enables
two-way data binding between components. (New easier method in .NET 8 ... bind-Value:after ... checkout Lecture 191)
<div class="form-group col-5">
<label>Country</label>
<InputSelect ValueExpression="@(() => city.CountryId)"
Value="@city.CountryId"
ValueChanged="@((int value) => OnValueChanged(value))"
class="form-select">
<option value="0" disabled="disabled" selected>Select Country</option>
@foreach (var cat in countries)
{
<option value="@cat.Id">@cat.CountryName</option>
}
</InputSelect>
<ValidationMessage For="()=>city.CountryId"></ValidationMessage>
</div>
@if (showCityDrop)
{
<div class="form-group col-7">
<label>City</label>
<InputSelect @bind-Value="city.CityName" class="form-select">
<option value="0" disabled="disabled" selected>Select City From Filtered Country</option>
@foreach (var cat in filteredCityList)
{
<option value="@cat.CityName">@cat.CityName</option>
}
</InputSelect>
<ValidationMessage For="()=>city.CountryId"></ValidationMessage>
</div>
}
//This method is called from Country DropDown List once country is chosen
//The CountryId is passed down to s
//We then cycle through all the cities looking for matching CountryIds in the allCities list
//When we find a match we store that city object in the filteredCityList
//.... we also grab the real CountryName (string) and store it in cName for future display on screen
private Task OnValueChanged(int s)
{
filteredCityList.Clear();
showCityDrop = false;
city.CityName = "";
if (s!=0)
{
foreach (var item in allCities)
{
if (item.CountryId == s)
{
filteredCityList.Add(item);
cName = item.Country.CountryName;
}
}
showCityDrop = true; //we passed down a legitimate country so we can now display the second DropDown List (Cities)
}
return Task.CompletedTask;
//Another way to fill the filtered CityList using a Lambda expression
//filteredCityList = allCities.Where(c => c.CountryId == s).ToList();
}
SimpleThreeTableDBapp
Three Table Application implementing the Radzen UI.
Department (Id, Name, Description)
Salary (Id, Level, Pay)
Employee ( Id, EmployeeName, DepartmentId, Department Department, SalaryId, Salary Salary) ... note the use of two foreign keys here ... Employee table needs to link (relate to) both the Department and Salary tables.
Key notes
EmployeesController
// GET: api/Employees
[HttpGet]
public async Task<ActionResult<IEnumerable<Employee>>> GetEmployee()
{
return await _context.Employee
.Include(u => u.Department)
.Include(u=>u.Salary)
.ToListAsync();
//return await _context.Employee.ToListAsync();
}
// GET: api/Employees/5
[HttpGet("{id}")]
public async Task<ActionResult<Employee>> GetEmployee(int id)
{
//var employee = await _context.Employee.FindAsync(id);
var employee = await _context.Employee
.Include(u => u.Department)
.Include(u=>u.Salary)
.FirstOrDefaultAsync(u => u.Id == id);
if (employee == null)
{
return NotFound();
}
return employee;
}
Each Table ... Department,Salary and Employee has a Edit Page and a Main Page as we have seen developed in previous applications. The key updates occur on the EmployeePage.razor and EmpEdit.razor pages.
EmployeePage.razor
<RadzenDataGrid Data="@employees" TItem="Employee" Style="width:1000px" AllowFiltering="true" AllowGrouping="true"
AllowColumnResize="true" AllowAlternatingRows="false" FilterMode="FilterMode.Advanced" AllowSorting="true" PageSize="3" AllowPaging="true" PagerHorizontalAlign="HorizontalAlign.Left" ShowPagingSummary="true"
LogicalFilterOperator="LogicalFilterOperator.Or">
<Columns>
<RadzenDataGridColumn TItem="Employee" Title="Action" Width="80px" Filterable="false" Sortable="false" Frozen="true">
<Template Context="employee">
<RadzenButton Icon="edit" Shade="Shade.Lighter" ButtonStyle="ButtonStyle.Primary" Click=@(args => OnClick(employee.Id)) />
</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="Employee" Property="EmployeeName" Title="Name" Frozen="true" Width="200px" TextAlign="TextAlign.Left" />
<RadzenDataGridColumn TItem="Employee" Property="Department.Name" Title="Department" Width="200px" />
<RadzenDataGridColumn TItem="Employee" Property="Department.Description" Title="Description" Width="120px" />
<RadzenDataGridColumn TItem="Employee" Property="Salary.Level" Title="Level" Width="300px" >
<Template Context="data">
@if (data.Salary.Level=="Management")
{
<span style="color:blue">@data.Salary.Level</span>
}
else
{
<span style="color:orangered">@data.Salary.Level</span>
}
</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="Employee" Property="Salary.Pay" Title="Pay" Width="160px" >
<Template Context="data">
@data.Salary.Pay.ToString("c")
</Template>
</RadzenDataGridColumn>
</Columns>
</RadzenDataGrid>
EmpEdit.razor
Code Section
private Employee? employee;
IList<Department>? departments;
IList<Salary>? salaries;
protected override async Task OnInitializedAsync()
{
departments = await Http.GetFromJsonAsync<IList<Department>>("Departments");
salaries = await Http.GetFromJsonAsync<IList<Salary>>("Salaries");
if (id == 0)
{
employee = new Employee();
}
else
{
employee = await Http.GetFromJsonAsync<Employee>($"Employees/{id}");
}
ready = true;
}
HTML Section
<div class="form-group col-5">
<label>Department</label>
<InputSelect @bind-Value="employee.DepartmentId" class="form-select">
<option value="0" disabled="disabled" selected>Select Department</option>
@foreach (var dept in departments)
{
<option value="@dept.Id">@dept.Name</option>
}
</InputSelect>
<ValidationMessage For="()=>employee.DepartmentId"></ValidationMessage>
</div>
<div class="form-group col-5">
<label>Salary Level</label>
<InputSelect @bind-Value="employee.SalaryId" class="form-select">
<option value="0" disabled="disabled" selected>Select Salary Level</option>
@foreach (var sal in salaries)
{
<option value="@sal.Id">@sal.Level</option>
}
</InputSelect>
<ValidationMessage For="()=>employee.SalaryId"></ValidationMessage>
</div>
In this Lecture we will
Learn what SignalR is and how it is used within an Blazor application to help create a real time app.
SignalR (Signal Realtime) is a library for ASP.NET developers to simplify the process of adding real time web functionality to applications. The best part is that this library is just a wrapper around web-standard technologies such as Web Sockets.
Real time web functionality via Socket connections are handy because they enable two-way communication between the server and client, meaning you can actually push data from the server to any browser which has an open connection (rather than have the client constantly ping the server for updated content).
SignalR is an excellent way to connect two or more clients together for real-time communication like
Chat applications
Games
Auctions
Financial transactions / Stock Quotes
Inventory Tracking
Let’s assume you have a page reporting a list of stocks, and any time one of these prices change, the HTML page needs to be refreshed.
Before SignalR, it was common to have JavaScript code using Ajax that periodically (for example, every 5 seconds) executed a GET request to the server, in order to retrieve possible new prices and display them in the HTML page.
Today, thanks to Blazor and its embedded SignalR functionality, we can invert this trend, and give the responsibility to the server to update the HTML page only when there is some new price to display.
Demo several simple examples of SignalR implementations
RealTimeChatMSdemo/RealTimeChatMSdemoUpdated (real time cross user communication)
.NET 8 Version (BlazorSignalRnet8) ... See Lectures 186-188 for a deeper dive
DBSignalRdemo (real time DB updates)
Learn how to set up SignalR in a new or existing application and how to connect to it using web clients.
We will use the sample application from the previous lecture (SimpleThreeTableDBApp) as our base of operation. This is a WebAssembly ASP.NET Core Hosted Application which implemented the Relational DB concepts covered in lectures 155-158. (Quick Demo...plus illustrate update issue ... refresh required).
In this simple example, we are going to see how to update an HTML page when a SQL Server table change occurs, without the need to reload the page or make asynchronous calls from the client to the server. (Simple3TableDBappSignalR)
The first thing we need to do is install the "Microsoft.AspNetCore.SignalR.Client" library using the NuGet package manager in the Client project (Right Click on ThreeTables.Client and pick Manage NuGet Packages ) . This library is already added to the Server project by default (Microsoft.AspNetCore.SignalR)
Next we must register the SignalR component in the Program.cs file of the ThreeTables.Server project
using ThreeTables.Server.Hubs;
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
//Additions for SignalR
builder.Services.AddSignalR();
builder.Services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
Now we create a folder in the Server project called Hubs and add a new class called BroadcastHub which inherits the Hub class from the SignalR library.
BroadcastHub is just a class that has exposed methods that can broadcast messages or whatever data you wish out to all the other clients
using Microsoft.AspNetCore.SignalR;
namespace ThreeTables.Server.Hubs
{
public class BroadcastHub : Hub
{
//This BroadcastHub class will
// contain exposed methods (public)
//that when you call them they can broadcast
//a message or data out to all the other clients
//Here we have added a method that
//will be used to send and receive push notifications
//to and from every client (All) using SignalR hub
//Think of this something like a server which we will
//constantly be accessing
public async Task SendMessages()
{
await Clients.All.SendAsync("ReceiveMessage");
}
//This method will send all the clients ReceiveMessage
//Now all the clients can implement handlers for ReceiveMessage
//Look for hubConnection.On("ReceiveMessage"
}
}
Now we go back to the Program.cs file to add the endpoints for the BroadcastHub class.
app.UseRouting();
app.MapRazorPages();
app.MapControllers();
app.MapHub<BroadcastHub>("/broadcastHub");
app.MapFallbackToFile("index.html");
app.Run();
All the initial prep work is now done and finally we can focus on our Client Project components.
We will focus on the associated pair of components EmployeePage.razor and EmpEdit.razor to illustrate the necessary updates required to the code. The same changes can then be applied to the DepartmentPage/DepartmentEdit and SalaryPage/SalaryEdit ... You work on those pages as an exercise
EmployeePage.razor
Add to the top
@page "/employeepage"
@using ThreeTables.Shared
@using Microsoft.AspNetCore.SignalR.Client
@inject HttpClient Http
@inject NavigationManager Nav
@implements IAsyncDisposable
In the Code Section
//Declare an instance of the HubConnection class which
//will be initialized inside the OnInitializedAsync method below
private HubConnection? hubConnection;
protected override async Task OnInitializedAsync()
{
//Here we initialize a SignalR hub connection
//and navigate to the broadcastHub endpoint
hubConnection = new HubConnectionBuilder()
.WithUrl(Nav.ToAbsoluteUri("/broadcastHub"))
.WithAutomaticReconnect()
.Build();
//This is our Event Handler
//Hub connection is listening for a new push message (ReceiveMessage)
//from the Hub server and it will call the LoadData method
//... If it does not get a push connection it still needs to
//start the connection to the Hub server
// and call the LoadData method to get the
//new or modified Employee data from the database
//Whenever we add or change a employee record in another web client
//it will be automatically reflected in this component.
//Hence we will get real time data
//The handler stays registered for the lifetime of the component even though the OnInitializedAsync() is only executed once.
hubConnection.On("ReceiveMessage", () =>
{
await LoadData();
StateHasChanged(); //causes component to be re-rendered
});
await hubConnection.StartAsync(); //starts connection to Hub server
await LoadData();
//old code below before SignalR implementation
//employees = await Http.GetFromJsonAsync<IList<Employee>>("Employees");
}
protected async Task LoadData()
{
employees = await Http.GetFromJsonAsync<IList<Employee>>("Employees");
StateHasChanged();
}
public bool IsConnected =>
hubConnection.State == HubConnectionState.Connected;
public async ValueTask DisposeAsync()
{
if (hubConnection is not null)
{
await hubConnection.DisposeAsync();
}
}
EmpEdit.razor
Add to the top
@page "/emp"
@page "/emp/{id:int}"
@using ThreeTables.Shared
@using Microsoft.AspNetCore.SignalR.Client
@inject HttpClient Http
@inject NavigationManager Nav
@implements IAsyncDisposable
In the Code Section (see EmpEditCodeSnippet for full details)
private HubConnection? hubConnection;
protected override async Task OnInitializedAsync()
{
departments = await Http.GetFromJsonAsync<IList<Department>>("Departments");
salaries = await Http.GetFromJsonAsync<IList<Salary>>("Salaries");
if (id == 0)
{
employee = new Employee();
}
else
{
employee = await Http.GetFromJsonAsync<Employee>($"Employees/{id}");
}
hubConnection = new HubConnectionBuilder() .WithUrl(Nav.ToAbsoluteUri("/broadcastHub"))
.WithAutomaticReconnect()
.Build();
await hubConnection.StartAsync();
ready = true;
}
private async Task HandleValidSubmit()
{
HttpResponseMessage response;
//Notice here how we send a notification to the hub server
//via the call to the SendMessage method (lambda expression)
//after saving the data (Post/Put/Delete) to the database
//Whenever we send a push notification from here all the
//other open clients will recieve the push notification
//and inside the EmployeePage.razor page
//the new current database contents will be re-rendered
if (employee.Id == 0)
{
response = await Http.PostAsJsonAsync("Employees", employee);
if (IsConnected) await SendMessage();
}
else
{
string requestUri = $"Employees/{employee.Id}";
response = await Http.PutAsJsonAsync(requestUri, employee);
if (IsConnected) await SendMessage();
}
if (response.IsSuccessStatusCode)
{
Nav.NavigateTo("employeepage");
}
else
{
error = response.ReasonPhrase;
}
}
private async Task DeleteEmployee()
{
string requestUri = $"Employees/{employee.Id}";
var response = await Http.DeleteAsync(requestUri);
if (IsConnected) await SendMessage();
if (response.IsSuccessStatusCode)
{
Nav.NavigateTo("employeepage");
}
else
{
error = response.ReasonPhrase;
}
}
public bool IsConnected => hubConnection.State == HubConnectionState.Connected;
//Make sure SendMessages matches name in BroadcastHub class exactly
Task SendMessage() => hubConnection.SendAsync("SendMessages");
//... or
//private async Task SendMessage()
//{
// if (hubConnection is not null)
// {
// await hubConnection.SendAsync("SendMessages");
// }
//}
Your Turn:
Code the remaining pages DepartmentPage/DepartmentEdit and SalaryPage/SalaryEdit
Literally use the exact same approach for the remaining component pages
Now that we have completed the coding we can run the application.
Go to the Employee Info page ... copy the url
Now go to a new tab and enter the same url
Drag the new tab away from the current browser and a new instance of the browser should activate.
Now modify a current record or add a new one and you should notice that the browsers will be automatically refreshed with the update data after saving in real time.
Revisit our opening demos and now take a look a the code with our new found knowledge of SignalR to make sure it makes sense.
RealTimeChatMSdemo
In the Server project Hubs folder ... ChatHub.cs . Note that SendMessage accepts two parameters user and message
public class ChatHub:Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
In the Index.razor page
@code {
private HubConnection? hubConnection;
private List<string> messages = new List<string>();
private string? userInput;
private string? messageInput;
protected override async Task OnInitializedAsync()
{
hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/chathub"))
.Build();
hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
{
var encodedMsg = $"{user}: {message}";
messages.Add(encodedMsg);
StateHasChanged();
});
await hubConnection.StartAsync();
}
private async Task Send()
{
if (hubConnection is not null)
{
await hubConnection.SendAsync("SendMessage", userInput, messageInput);
}
}
public bool IsConnected =>
hubConnection?.State == HubConnectionState.Connected;
public async ValueTask DisposeAsync()
{
if (hubConnection is not null)
{
await hubConnection.DisposeAsync();
}
}
}
HTML Section
@page "/"
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager Navigation
@implements IAsyncDisposable
<PageTitle>Index</PageTitle>
<div class="form-group">
<label>
User:
<input @bind="userInput" />
</label>
</div>
<div class="form-group">
<label>
Message:
<input @bind="messageInput" size="50" />
</label>
</div>
<button @onclick="Send" disabled="@(!IsConnected)">Send</button>
<hr>
<ul id="messagesList">
@foreach (var message in messages)
{
<li>@message</li>
}
</ul>
RealTimeChatMSdemoUpdated
In the Hubs folder
public class ChatHub:Hub
{
public async Task SendMessage(string message)
{
await Clients.All.SendAsync("ReceiveMessage", message);
}
}
In the Index.razor page
Code Section
hubConnection.On<string>("ReceiveMessage", (message) =>
{
messageInput = message;
StateHasChanged();
});
private async Task TextKeyPressed(KeyboardEventArgs args)
{
await hubConnection.SendAsync("SendMessage", messageInput);
}
HTML Section
<textarea @bind="messageInput"
@bind:event="oninput"
@onkeyup="TextKeyPressed"
rows="5"
style="width;100%">
</textarea>
DBSignalRdemo
Simple Database application which stores Book Names, ISBN, Author and Price with full CRUD ability and of course with a SignalR implementation.
Offer a Suggested Exercise and Highlight several simple applications of SignalR
SignalRExerciseSuggestion/ProjectTrackerDBSignalR
This application serves as a Review and Extension of some basic Blazor SignalR concepts ... including
Modifying an existing application to implement SignalR (Lecture 152 ProjectTrackerDB)
Modifying/Redoing a .NET 5 app to work in .NET 6 (No more startup.cs)
Click on the Tracker link on the left to try out the Application
Remember to test out it's' Real Time capabilities ... create a second tab and tear it away from the browser to create the second instance
Drag and Drop Projects and watch them instantly appear in the second browser screen
Note the key updates
<DropZone Status="ProjectStatus.Requested"
ProjectItems="ProjItems"
OnDrop="OnDropAsync"
OnStartDrag="OnStartDrag" />
private async Task OnDropAsync(ProjectStatus status)
{
CurrentItem.Status = status;
string requestUri = $"ProjectItems/{CurrentItem.Id}";
var response = await Http.PutAsJsonAsync(requestUri, CurrentItem);
if (IsConnected) await SendMessage();
}
public bool IsConnected => hubConnection.State == HubConnectionState.Connected;
Task SendMessage() => hubConnection.SendAsync("SendMessages");
BlazorStateManagementSignalR
This application serves as a Review and Extension of some of the State Management skills covered in Lecture 94 ... with the addition of SignalR
State Management is one of the much needed features in modern web apps ... recall the issue when we move between various pages (ie Counter page) ... we don't want to lose values of fields and properties
With State Management the data is managed in the browser using an in memory state container service and frequent postbacks to the server can be avoided.
In Blazor applications we can easily implement State Management using a Global State Container Object (See SessionState folder) because the application with it's dependencies is loaded and executed in the browser.
This updated application now incorporates the new ideas covered in Lecture 159 and allows multiple clients to access the Counter page with the Current Count synchronized throughout....Notice how the counting stops for all users once we get to a count of 50.
But note, this is a simplistic solution which requires all the clients to begin at approximately the same time. Can you figure out how to synchronize clients who join in much later in the counting process? ... Might need a Database connection
This Real Time updating across multiple clients opens up all kinds of potential apps including Surveys,Voting,Event participant tracking etc
Key updates
In the SessionState folder of the Client project we create a CounterState.cs class
//To maintain the state of data across sender and receiver
//components we create a global session state container service class
public class CounterState
{
public int CurrentCount { get; set; }
}
//Remember to declare existence of service in Program.cs
//builder.Services.AddSingleton<CounterState>();
In the Hubs folder in the Server project we create our BroadcastHub.cs class
public class BroadcastHub:Hub
{
//c is the current count from the Counter.razor page
public async Task SendMessages(int c)
{
await Clients.All.SendAsync("ReceiveMessage",c);
}
}
In the Counter.razor page
The HTML is basically the same
@inject BlazorStateManagementSignalR.Client.SessionState.CounterState CounterState
@if (CurrentCount <50)
{
<p role="status">Current count: @CurrentCount</p>
}
else
{
<p role="status">Max Value of 50 reached no further entries allowed</p>
}
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
int CurrentCount;
private HubConnection? hubConnection;
protected override async Task OnInitializedAsync()
{
hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/broadcastHub"))
.WithAutomaticReconnect()
.Build();
//This is our Event Handler
//hubConnection is listening for a new push message (ReceiveMessage)
//from the Hub server and it will take the passed in c (current value of count)
//a store it into CurrentCount
//we also store this value in our global session state container
//called CounterState.CurrentCount
hubConnection.On<int>("ReceiveMessage", (c) =>
{
CurrentCount = c;
CounterState.CurrentCount = CurrentCount;
StateHasChanged();
});
//Whether we get a push connection or not we still need
//to start the connection to the Hub server
//and locally store our global session state value into CurrentCount
await hubConnection.StartAsync();
CurrentCount = CounterState.CurrentCount;
StateHasChanged();
}
private async void IncrementCount()
{
//Local Session State work ... before SignalR work
CurrentCount = CounterState.CurrentCount;
CurrentCount++;
CounterState.CurrentCount = CurrentCount;
//We updated the counter (added 1)
//Now we can send a notification to the hub server
//via the call to the SendMessage method (lambda expression)
if (IsConnected) await SendMessage();
}
public bool IsConnected =>
hubConnection?.State == HubConnectionState.Connected;
//Here we push a notification to all the open clients
//and we include the global session state CounterState.CurrentCount)
Task SendMessage() => hubConnection.SendAsync("SendMessages",CounterState.CurrentCount);
In this Lecture we will
Revisit/Recall how we have used simple animation gifs and Font Awesome to display simple loading animations in previous applications (COVID9AnimatedLoadingGIF.rar)
Learn how to implement some simple Animation Effects using the AOS (Animate on Scroll) Cascading Style Sheet Library (https://github.com/michalsnik/aos)
Create a WebAssembly Application (BlazorAnimationEffectCSSpart1)
First we set up the library in index.html (wwwroot folder)
Add style in <head>:
<link rel="stylesheet" href="https://unpkg.com/aos@next/dist/aos.css" />
Add script right before closing </body> tag
<script src="https://unpkg.com/aos@next/dist/aos.js"></script>
Add new js folder in wwwroot and add animation.js
window.addEventListener("load", function () {
AOS.init();
});
... and lastly add reference to this new Javascript file above in index.html
src="/js/animation.js"
Next we create a non-routable Razor Component (Animation.razor) which we will eventually reference in the Counter.razor page
<div data-aos="@_animationName" data-aos-delay="@_delay" data-aos-duration="@_duration">
@ChildContent
</div>
@code {
private string _animationName = "";
private string _duration = "";
private string _delay = "";
[Parameter]
public RenderFragment ChildContent { get; set; }
protected override void OnParametersSet()
{
_animationName = "fade-up";
_duration="8000";
_delay="500";
}
}
... and finally in the Counter.razor page
<Animation>
<button class="btn btn-primary >Click me </button>
</Animation>
BlazorAnimationEffectCSSpart1Updated
In the Counter.razor page
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-danger" @onclick="@( () => IsVisible=!IsVisible)">Click to Toggle Button Animation</button>
<br/>
<br />
@if (IsVisible)
{
<Animation>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</Animation>
}
@code {
private int currentCount = 0;
private bool IsVisible = false;
private void IncrementCount()
{
currentCount++;
}
}
Adding a class structure (BlazorAnimationEffectCSSpart2 )
First we create a folder called Models and create a class AnimationName.cs ... note the use of static which allows us to access this class and it's properties without having to instantiate it.
public static class AnimationName
{
public const string fade = "fade";
public const string fadeUp = "fade-up";
public const string fadeDown = "fade-down";
public const string fadeLeft = "fade-left";
public const string fadeRight = "fade-right";
}
Now back to our non-routable component Animation.razor we update the code section.
[Parameter]
public string AnimationNameSelect { get; set; } = string.Empty;
protected override void OnParametersSet()
{
_animationName = "fade-up";
_duration="8000";
_delay="500";
if(!string.IsNullOrEmpty(AnimationNameSelect))
{
_animationName = AnimationNameSelect;
}
}
... and lastly we go back to the Counter.razor page and update our reference to the Animation component.
@page "/counter"
@using BlazorAnimations.Models
<PageTitle>Counter</PageTitle>
<Animation AnimationNameSelect="@animationSelect2">
<h1>Counter</h1>
</Animation>
<p role="status">Current count: @currentCount</p>
<Animation AnimationNameSelect="@animationSelect1">
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</Animation>
public string animationSelect1 = "";
public string animationSelect2 = "";
protected override void OnInitialized()
{
animationSelect1 = AnimationName.fadeLeft;
animationSelect2 = AnimationName.fadeDown;
}
BlazorAnimationEffectCSSpart2Updated
In the Counter.razor page
@if(IsVisible)
{
<Animation AnimationNameSelect="@animationSelect2">
<h1 class="alert-primary">Counter</h1>
</Animation>
}
<br/>
<button class="btn btn-danger" @onclick="@( () => IsVisible=!IsVisible)">Click to Toggle Animation</button>
<br/>
<br/>
<p class="@(IsVisible? "btn btn-warning":"")"><em>Current count: @currentCount</em></p>
@if (IsVisible)
{
<Animation AnimationNameSelect="@animationSelect1">
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</Animation>
}
In this Lecture we will
Create two WebAssembly applications which use simple techniques to move graphic objects around the screen using buttons and the keyboard
BlazorGraphicsMovingObjects1
In the Index.razor page we will add two buttons Across and Down which will increment the left and top properties of a div container which has an image embedded in it
<button class="btn btn-primary" @onclick="IncrementLeft">Across</button>
<button class="btn btn-primary" @onclick="IncrementTop">Down</button>
<div style="top:@(top)px; left:@(left)px; position:absolute">
<img src="images/daffy.png" />
</div>
@code {
private int top = 200;
private int left = 250;
private void IncrementLeft()
{
left += 10;
}
private void IncrementTop()
{
top += 10;
}
}
For a better visual go to the MainLayout.razor and remove almost everything except
@inherits LayoutComponentBase
@Body
BlazorGraphicsMovingObjects2
In this second version we will move the graphic object using the keyboard and simulate a Game Loop which continually calls an Update (updates image position) and Draw (redraws screen ) method using a simple timer.
First we will remove the buttons and replace them with a new div which has keydown and keyup event handlers and a @ref="mainDiv" ... which we will declare as an ElementReference in the Code section
@using System.Timers
@*80vw means 80% of window width*@
<div @onkeydown="HandleKeyDown" @onkeyup="HandleKeyUp" @onkeydown:preventDefault
style="background-color: blue; width: 80vw; height: 80vh; margin: auto"
tabindex="0" @ref="mainDiv">
<div style="color: white; top: @(_top)px; left: @(_left)px; width: 20px; position: relative">
<img src="images/cartoon.png" />
</div>
</div>
Next we make a number of new declarations in the Code section
private const int SPEED = 3;
private int _top = 300;
private int _left = 0;
private int _forceUp = 0;
private int _forceRight = 0;
private ElementReference mainDiv;
private Timer _timer;
To kick start the animation we must give our object focus
OnAfterRenderAsync(bool firstRender)
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await mainDiv.FocusAsync();
}
}
OnAfterRenderAsync is called after a component has finished rendering. Element and component references are populated at this point.
The firstRender parameter is set to true the first time that the component instance is rendered. This can be used to ensure that initialization work is only performed once.
Then we activate our Game Loop by starting a timer in the
protected override Task OnInitializedAsync()
{
_timer = new Timer();
_timer.Interval = 16;
_timer.Elapsed += TimerElapsed; //timer to be used below initialized here
_timer.AutoReset = true;
_timer.Enabled = true;
return base.OnInitializedAsync();
}
//Game loop which continually calls Update and Draw using a simple timer
private void TimerElapsed(Object source, System.Timers.ElapsedEventArgs e)
{
Update();
Draw();
}
private void Update()
{
check for left and top wall hit ... in our last lecture we will check for all four walls
_left += _forceRight;
_top += _forceUp;
}
//Blazor doesn't always automatically refresh state
private void Draw() => this.StateHasChanged();
... And finally dealing with the KeyUp and KeyDown events
private void HandleKeyDown(KeyboardEventArgs e)
{
switch (e.Code)
{
case "ArrowLeft": // Left
WalkAcross(-SPEED);
break;
case "ArrowUp": // Up
WalkUpDown(-SPEED);
break;
case "ArrowDown": //down
WalkUpDown(SPEED);
break;
case "ArrowRight": // Right
WalkAcross(SPEED);
break;
default:
break;
}
}
//This stops walking when key is released
private void HandleKeyUp(KeyboardEventArgs e)
{
switch (e.Code)
{
case "ArrowLeft": // Left
WalkAcross(0);
break;
case "ArrowRight": // Right
WalkAcross(0);
break;
case "ArrowUp":
WalkUpDown(0);
break;
case "ArrowDown":
WalkUpDown(0);
break;
default:
break;
}
}
We have two lambda expressions to update the amount to move left/right and up/down
private void WalkAcross(int amt) => _forceRight = amt;
private void WalkUpDown(int amt) => _forceUp = amt;
Supplementary Demo
BlazorGraphicsMovingObjects2PeriodicTimer
This version uses the PeriodicTimer class which is somewhat easier (shorter code) to use.
Key updates
//private Timer _timer;
PeriodicTimer? timer;
protected override async Task OnInitializedAsync()
{
// _timer = new Timer();
// _timer.Interval = 16;
// _timer.Elapsed += TimerElapsed; //timer to be used below initialized here
// _timer.AutoReset = true;
// _timer.Enabled = true;
timer = new PeriodicTimer(TimeSpan.FromSeconds(.01));
await Start();
}
Don't use TimerElapsed method anymore
private async Task Start()
{
while (await timer.WaitForNextTickAsync())
{
Update();
Draw();
}
}
In this Lecture we will
Learn that currently there is no direct Canvas support in WebAssembly. In order to draw using Canvas, you can use the third-party, free Blazor.Extensions.Canvas library.
<canvas> is an HTML 5 element which can be used to draw graphics via scripting (usually JavaScript). This can, for instance, be used to draw graphs, combine photos, or create simple animations. First introduced in WebKit by Apple for the macOS Dashboard, <canvas> has since been implemented in browsers.
Learn how to Add Blazor.Extensions.Canvas NuGet to your project
https://github.com/BlazorExtensions/Canvas
Create a simple intro example (BlazorCanvasIntro) that displays some text , and rectangle .
index.html
<script src='_content/Blazor.Extensions.Canvas/blazor.extensions.canvas.js'></script>
index.razor
@page "/"
@using Blazor.Extensions;
@using Blazor.Extensions.Canvas
@using Blazor.Extensions.Canvas.Canvas2D;
<BECanvas Width="300" Height="400" @ref="_canvasReference"></BECanvas>
@code {
private Canvas2DContext _context;
protected BECanvasComponent _canvasReference;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
_context = await _canvasReference.CreateCanvas2DAsync();
await _context.SetFontAsync("48px serif");
await _context.StrokeTextAsync("Hello Blazor!!!", 10, 100);
await _context.SetFillStyleAsync("green");
await _context.FillRectAsync(10, 150, 100, 100);
await _context.SetFillStyleAsync("blue");
await _context.SetFontAsync("20px Segoe UI");
await _context.FillTextAsync("Simple Canvas Demo ... " , 10, 350);
}
}
Supplementary Demos
BlazorCanvasIntroChartingDemo
Gives the user the option to display Horizontal and Vertical Bar Charts
Two arrays are harded coded with Names and Sales (next demo loads data in from Database)
string[] salespeople = new string[] { "Tom", "Mary", "John","Cathy","Jen" };
int[] totals = new int[] { 200, 450, 74,300,550 };
The sales are "Scaled" to fit our designated screen size
<BECanvas Width="600" Height="400" @ref="_canvasReference"></BECanvas>
<div style="margin-left:30px">
<button class="btn btn-primary" @onclick="@ShowHorizontal">Display Horizontal Bar Chart</button>
<button class="btn btn-primary" @onclick="@ShowVertical">Display Vertical Bar Chart</button>
</div>
private double Scaling(int[] t,string align)
{
double big = -1000;
double scalingfactor = 0;
for (int i=0;i<t.Length;i++)
{
if (t[i]> big)
{
big = t[i];
}
}
if (align=="h")
{
scalingfactor = big / (600-120); //we are not really using the entire width of 600
//we start drawing at 120 over from the left so take that amout away from width
}
else if (align=="v")
{
scalingfactor = big / (400 - 100); //only using 300 of the actual 400 height because of top title and bottom labels
}
return scalingfactor;
}
private async Task ShowHorizontal()
{
await _context.ClearRectAsync(0, 0, 600, 400);
await _context.SetFillStyleAsync("blue");
await _context.SetFontAsync("20px Segoe UI");
await _context.FillTextAsync("Simple Canvas Demo ... Bar Chart Application", 10, 30);
//The X is displayed to give us an idea where the right edge of the canvas is
//... strictly for instructional purposes ... not part of Bar Chart
await _context.SetFillStyleAsync("blue");
await _context.SetFontAsync("16px serif");
await _context.FillTextAsync("X", 590, 55);
double sf = Scaling(totals,"h");
await _context.SetFillStyleAsync("blue");
await _context.SetFontAsync("16px serif");
await _context.FillTextAsync("Sales", 10, 55);
await _context.SetFillStyleAsync("blue");
await _context.SetFontAsync("16px serif");
await _context.FillTextAsync("Name", 65, 55);
for (int i=0;i<totals.Length;i++)
{
//display text first
await _context.SetFontAsync("16px serif");
await _context.SetFillStyleAsync("blue");
await _context.FillTextAsync(totals[i].ToString() , 10, 88+i*30);
await _context.SetFontAsync("16px serif");
await _context.SetFillStyleAsync("blue");
await _context.FillTextAsync(salespeople[i].ToString() , 65, 88 + i * 30);
//now work on length of bar for corresponding total
int x = (int)(totals[i] / sf + .5); //.5 rounds up to next integer
//pick a random color to use for bar ... in Hex notation #4B89FF
var c = r.Next(0xFFFFFF); //generates a random 24 bit number
string nc = "#" + c.ToString("X6"); //X produces single digit hex number
//X6 produces 6 digit hex number
//draw the bar to length x
await _context.SetFillStyleAsync(nc);
await _context.FillRectAsync(120, 80+i*30, x, 10);
}
}
ExpenseTrackerUpdateCanvasChart /ExpenseTrackerUpdateCanvasChartUpdate
This is a redo/update of the Expense Tracker Relational DB application created in Lecture 158. Here we chart the Total Expenses for each Expense Type (Airfare, Lodging,Meal, Misc, PD) ... may need to execute using "Start without Debugging"
Start off with some key declarations
//expenses that will be loaded in from DB
IList<Expense> expenses;
private IList<ExpenseType> types;
// unlimited expense types ... arrays declared here and are sized (initialized) in NewTotalExpenses method
int ExpenseTypeCount;
string[] NewexpenseNames;
double[] NewexpenseTotals;
Then we load in our Lists
protected override async Task OnInitializedAsync()
{
expenses = await Http.GetFromJsonAsync<IList<Expense>>("Expenses");
types = await Http.GetFromJsonAsync<IList<ExpenseType>>("ExpenseTypes");
}
Our Charting Tasks ShowHorizontal and ShowVertical will need to call a method to Total the expenses ... NewTotalExpenses()
//new method with variable number of unknown Expense Types
private void NewTotalExpenses()
{
ExpenseTypeCount = 0;
//count the number of expense types ... key to process
//Now we can declare the size of our arrays
//that will hold the ExpenseType Names and the Accumulated amounts of each type
foreach (var cat in types)
{
ExpenseTypeCount++;
}
//Initialize size of arrays to ExpenseTypeCount
NewexpenseNames = new string[ExpenseTypeCount] ;
NewexpenseTotals = new double [ExpenseTypeCount] ;
//Store the actual name of the expense type in new array called NewexpenseNames
//... and at the same time Initialize accumulator array to track totals for each unique expense type
int count = 0;
foreach (var cat in types)
{
NewexpenseNames[count] = cat.Type;
NewexpenseTotals[count] = 0;
count++;
}
//Go through every item in the expenses database
//See which expense type it has connected to it (we use a for loop here to check every possibilty available)
//when we find a match ... note it as index i ... if it's Airfare for instance the index would be 1
// ... now use that as the index of the accumulator ... NewexpenseTotals[1]= NewexpenseTotals[1] + (double)item.Amount
foreach (var item in expenses)
{
for (int i=0;i<ExpenseTypeCount;i++)
{
if (item.ExpenseType.Type==NewexpenseNames[i])
{
NewexpenseTotals[i] += (double)item.Amount;
}
}
}
//We now have everything we need to create our Bar Charts
//The names of all the Expense Types for the charts
//The totals for each Expense Type
}
Here is the Vertical BarChart
private async Task ShowVertical()
{
//TotalExpenses(expenseTotals);
NewTotalExpenses();
await _context.ClearRectAsync(0, 0, 600, 400);
await _context.SetFillStyleAsync("red");
await _context.FillRectAsync(0, 0, 600, 1);
await _context.FillRectAsync(0, 0, 1, 400);
await _context.FillRectAsync(599, 0, 1, 400);
await _context.FillRectAsync(0, 399, 600, 1);
await _context.SetFillStyleAsync("blue");
await _context.SetFontAsync("20px Segoe UI");
await _context.FillTextAsync("Simple Canvas Demo ... Bar Chart Application" , 10, 30);
await _context.DrawImageAsync(_spritesheet1, 450, 0, 100, 80);
//double sf = Scaling(expenseTotals, "v");
double sf = Scaling(NewexpenseTotals, "v");
for (int i=0;i<NewexpenseTotals.Length;i++)
//for (int i=0;i<expenseTotals.Length;i++)
{
//display bottom text first before vertical bar chart
await _context.SetFontAsync("16px serif");
await _context.SetFillStyleAsync("blue");
//await _context.FillTextAsync(expenseNames[i].ToString(), 10 + i * 70, 400 - 10);
await _context.FillTextAsync(NewexpenseNames[i].ToString(), 10 + i * 70, 400 - 10);
//now work on length of bar for corresponding total
//int y = (int)(expenseTotals[i] / sf + .5); //.5 rounds up to next integer
int y = (int)(NewexpenseTotals[i] / sf + .5); //.5 rounds up to next integer
//pick a random color to use for bar ... in Hex notation #4B89FF
var c = r.Next(0xFFFFFF); //generates a random 24 bit number
string nc = "#" + c.ToString("X6"); //X produces single digit hex number
//X6 produces 6 digit hex number
//draw the bar to length (height) y
await _context.SetFillStyleAsync(nc);
await _context.FillRectAsync(20+i*70, 400-25-y, 10, y);
//add totals to top of vertical bar chart
await _context.SetFontAsync("16px serif");
await _context.SetFillStyleAsync("blue");
//await _context.FillTextAsync(expenseTotals[i].ToString("c"), 10 + i * 70, 400 - 25 - y - 20);
await _context.FillTextAsync(NewexpenseTotals[i].ToString("c"), 10 + i * 70, 400 - 25 - y - 20);
}
await _context.SetFontAsync("20px Segoe UI");
await _context.SetFillStyleAsync("green");
//await _context.FillTextAsync("Grand Total of Expenses: " + expenseTotals.Sum().ToString("c"), 250, 200);
await _context.FillTextAsync("Grand Total of Expenses: " + NewexpenseTotals.Sum().ToString("c"), 250, 100);
}
In this Lecture we will
Create a series of applications to highlight how to create some simple animations using Blazor
Learn some basic animation techniques by incorporating a
Game Loop
Update method
Render method
Create a Timer Display App (Blazor2Dpart1TimerSpriteDisplay)
Using the previous Lecture’s application as our base (BlazorCanvasIntro)
First we add a reference to our page (Index.razor) so we can make calls to and from Javascript functions via JSInterop
@inject IJSRuntime JsRuntime;
Next we add a canvas element
<div id="theCanvas" style="position:fixed; opacity:1; background-color:blue;width:100%; height:100%">
<BECanvas Width="300" Height="400" @ref="_canvasReference"></BECanvas>
</div>
We also declare two images ... initially hidden
<img @ref="_spritesheet1" hidden src="images/daffy.png" />
<img @ref="_spritesheet2" hidden src="images/cartoon.png" />
Notice the @ref references ... we need to declare them in the code section
protected BECanvasComponent? _canvasReference;
Canvas2DContext _outputCanvasContext;
ElementReference _spritesheet1;
ElementReference _spritesheet2;
Next we use the reference to the Canvas element to generate the wrapper
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_outputCanvasContext = await _canvasReference.CreateCanvas2DAsync();
}
}
Notice that we do not display anything to the screen here (compared to BlazorCanvasIntro from previous Lecture). We are going to put that code in a separate method (GameLoop) that will form part of our Render loop
Now we need to make a key Initialization to kick start our animation. The Javascript function initGame that we are calling is located in index.html in wwwroot folder
protected override async Task OnInitializedAsync()
{
await JsRuntime.InvokeAsync<object>("initGame", DotNetObjectReference.Create(this));
}
Javascript cannot reference .NET object instances directly, so Blazor offers us a helper here; the very aptly named DotNetObjectReference class. It offers us a single static method called Create(). On the Javascript side, the passed DotNetObjectReference exposes a method called invokeMethodAsync(). This is used to invoke methods associated with that particular .NET object instance.
The function initGame, is used to subscribe to some useful events on the window and the canvas objects. Go to the index.html file to add the following code.
function gameLoop(timeStamp) {
theInstance.invokeMethodAsync('GameLoop', timeStamp);
window.requestAnimationFrame(gameLoop);
//Begins the render loop
//On every call to requestAnimationFrame we will invoke a C# function/method called GameLoop (located in Index.razor)
//which will update the canvas' status and displays the application time
}
window.initGame = (instance) => {
window.theInstance = instance;
window.requestAnimationFrame(gameLoop);
};
Wondering about timeStamp .... it's part of requestAnimationFrame and indicates the current time (based on the number of milliseconds since time origin).
Note the call to GameLoop which will be located on the Index.razor page. So here we are calling a C# function from a Javascript function. Note the use of [JSInvokable]
[JSInvokable]
public async ValueTask GameLoop(float timeStamp)
{
// updates the state & renders current frame
await _outputCanvasContext.ClearRectAsync(0, 0, 300, 400);
await _outputCanvasContext.SetFillStyleAsync("yellow");
await _outputCanvasContext.FillRectAsync(10, 50, 300, 100);
await _outputCanvasContext.SetFillStyleAsync("black");
await _outputCanvasContext.SetFontAsync("24px Arial");
await _outputCanvasContext.FillTextAsync("Time: " + timeStamp, 20, 80);
}
Let's finish off by adding our two images (sprites ...not moving yet!)
In GameLoop
await _outputCanvasContext.DrawImageAsync(_spritesheet1, 10, 200, 100, 100);
await _outputCanvasContext.DrawImageAsync(_spritesheet2, 100, 200, 100, 100);
Demonstrate a simple application that moves sprites across the screen (Blazor2DPart2SpriteMove)
Deals with wall collisions ... Displays a background image ...Handles screen(window) resizing
Key concepts and updates
The first thing to do is to update our index.html and add the code to handle window resizing
Few things going on here:
We’re attaching a handler to the window resize event in initGame … which will now be called from OnAfterRenderAsync instead of OnInitializedAsync
in onResize() we store the new window size
we’ve updated the call to GameLoop() to receive the new size
<script>
function gameLoop(timeStamp) {
window.requestAnimationFrame(gameLoop);
game.instance.invokeMethodAsync('GameLoop', timeStamp, game.canvas.width, game.canvas.height);
}
function onResize() {
if (!window.game.canvas)
return;
game.canvas.width = window.innerWidth;
game.canvas.height = window.innerHeight;
}
window.initGame = (instance) => {
var canvasContainer = document.getElementById('canvasContainer'),
canvases = canvasContainer.getElementsByTagName('canvas') || [];
window.game = {
instance: instance,
canvas: canvases.length ? canvases[0] : null
};
window.addEventListener("resize", onResize);
onResize();
window.requestAnimationFrame(gameLoop);
};
</script>
The next step is to update the Index.razor page.
First we have to add the images to render. The hidden attribute is necessary to hide the image, otherwise will be displayed right before the canvas. Here we add two images that will move and a background image
@using System.Drawing
<img @ref="_spritesheet1" hidden src="images/daffy.png" />
<img @ref="_spritesheet2" hidden src="images/cartoon.png" />
<img @ref="_spritesheet3" hidden src="images/sjb.jpg"/>
@using System.Drawing ... this will be used with the Point class reference
Now in the Code Section
First we declare our images from the HTML section of type ElementReference
ElementReference _spritesheet1;
ElementReference _spritesheet2;
ElementReference _spritesheet3;
Next we initialize starting positions and directions for the two images that are going to move across the screen. We also initialize there speed of movement
Point _spritePosition1 = new Point(0,0);
Point _spritePosition2 = new Point(140,240);
Point _spriteDirection1 = new Point(1, 1);
Point _spriteDirection2 = new Point(1, 3);
float _spriteSpeed = 5f;
Next declare a Sprite class in the Models folder
public class Sprite
{
public Size Size { get; set; }
public ElementReference SpriteSheet { get; set; }
}
…. We declare the 3 instances of the Sprite class
Sprite? _sprite1;
Sprite? _sprite2;
Sprite? _sprite3;
... and then create 3 instances of the Sprite class … one for each image in the OnAfterRenderAsync Task
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
return;
await JsRuntime.InvokeAsync<object>("initGame", DotNetObjectReference.Create(this));
_context = await _canvas.CreateCanvas2DAsync();
_sprite1 = new Sprite()
{
Size = new Size(200, 200),
SpriteSheet = _spritesheet1
};
_sprite2 = new Sprite()
{
Size = new Size(200, 200),
SpriteSheet = _spritesheet2
};
_sprite3 = new Sprite()
{
Size = new Size(600, 600),
SpriteSheet = _spritesheet3
};
}
The GameLoop calls Update and Render methods
public async ValueTask GameLoop(float timeStamp, int screenWidth, int screenHeight)
{
await Update(screenWidth, screenHeight);
await Render(screenWidth, screenHeight);
}
The Update method updates the sprite positions (with a speed multiplier) and checks for wall collisions
private async ValueTask Update(int screenWidth, int screenHeight)
{
if (_spritePosition1.X + _sprite1.Size.Width >= screenWidth || _spritePosition1.X < 0)
{
_spriteDirection1.X = -_spriteDirection1.X;
wallCount++;
}
if (_spritePosition1.Y + _sprite1.Size.Height >= screenHeight || _spritePosition1.Y < 0)
{
_spriteDirection1.Y = -_spriteDirection1.Y;
wallCount++;
}
if (_spritePosition2.X + _sprite2.Size.Width >= screenWidth || _spritePosition2.X < 0)
_spriteDirection2.X = -_spriteDirection2.X;
if (_spritePosition2.Y + _sprite2.Size.Height >= screenHeight || _spritePosition2.Y < 0)
_spriteDirection2.Y = -_spriteDirection2.Y;
_spritePosition1.X += (int)(_spriteDirection1.X*_spriteSpeed);
_spritePosition1.Y += (int)(_spriteDirection1.Y*_spriteSpeed);
_spritePosition2.X += (int)(_spriteDirection2.X * _spriteSpeed);
_spritePosition2.Y += (int)(_spriteDirection2.Y * _spriteSpeed);
}
The Render method does the actual drawing on screen
1. Draws the Background
2. Draws the two images at their current X , Y coordinates
3. Displays a Title
4. Displays a count of the number of wall hits for one of the images
private async ValueTask Render(int width, int height)
{
await _context.BeginBatchAsync();
await _context.ClearRectAsync(0, 0, width, height);
await _context.DrawImageAsync(_sprite3.SpriteSheet, 0,0, width,height);
await _context.DrawImageAsync(_sprite1.SpriteSheet, _spritePosition1.X, _spritePosition1.Y, _sprite1.Size.Width, _sprite1.Size.Height);
await _context.DrawImageAsync(_sprite2.SpriteSheet, _spritePosition2.X, _spritePosition2.Y, _sprite2.Size.Width, _sprite2.Size.Height);
await _context.SetFillStyleAsync("#FFFFFF");
await _context.SetFontAsync("20px Segoe UI");
await _context.FillTextAsync("Animation Demo ... " + wallCount.ToString(), 10, 40);
await _context.EndBatchAsync();
}
In this Lecture we will
Start off by highlighting the Supplementary demos from Lecture 162 (Intro to using the HTML 5 Canvas API)
BlazorCanvasIntroChartingDemo
ExpenseTrackerUpdateCanvasChartUpdate
A good first attempt at a beginners level ... but we can do better
Learn that ApexCharts is a free open-source JavaScript library to generate interactive and responsive charts. It has a wide range of chart types. It is the best free library that I found for working with charts, with animations.
Learn that ApexCharts for Blazor is a wrapper library for working with ApexCharts in Blazor. It provides a set of Blazor components that makes it easier to use the charts within Blazor applications.
Create several simple applications that demonstrate the capabilities of the ApexCharts Library.
ApexChartDemo1 (bar graph)
Install the Nuget Package Blazor-ApexCharts
In the index.html add the following lines to the body tag after the _framework reference (Note: Version 2 It's no longer necessary to manually include these javascript files)
src="_content/Blazor-ApexCharts/js/apex-charts.min.js"
src="_content/Blazor-ApexCharts/js/blazor-apex-charts.js"
Add a reference in _Imports.razor
@using ApexCharts;
We are going to create a simple application to display gross and net export values for countries
First we declare a class called Order in the Shared project
public class Order
{
public string Country { get; set; }
public decimal GrossValue { get; set; }
public decimal NetValue { get; set; }
}
Now we work on the graph generation in the Index.razor page (remove all existing content)
First make a couple of declarations at the top
@page "/"
@using ApexChartDemo.Shared
Next lets go into the code section and declare a List of orders and load in (hard coded) some actual values.
private List<Order> orders { get; set; }
protected override async Task OnInitializedAsync()
{
await LoadData();
}
private async Task LoadData()
{
//Simulate External Api call ... harded coded for now
await Task.Delay(3000);
//orders = SampleData.GetOrders();
orders = new()
{
new()
{
Country="Canada",
GrossValue=123456.75m,
NetValue=100000m
},
new()
{
Country="USA",
GrossValue=2873456m,
NetValue=87345m
},
new()
{
Country="India",
GrossValue=723456.75m,
NetValue=300000m
},
new()
{
Country="Germany",
GrossValue=543456m,
NetValue=387345m
}
};
}
Alternate Solutions
orders = new List<Order>
{
new Order{Country="Canada",GrossValue=123456.75m,NetValue=100000m},
new Order{Country="USA",GrossValue=2873456m,NetValue=87345m}
};
Order x = new Order()
{
Country="Canada",
GrossValue=123456.75m,
NetValue=100000m
};
orders.Add(x);
Order x = new Order();
x.Country = "Canada";
x.GrossValue = 133456.75m;
x.NetValue = 100000m;
orders.Add(x);
If we add a constructor to our Order class
public Order(string country, decimal grossValue, decimal netValue)
{
Country = country;
GrossValue = grossValue;
NetValue = netValue;
}
..... then we can use
orders = new List<Order>
{
new Order("Canada",123456.75m,100000m),
new Order("USA",2873456m,87345),
new Order("India",723456.75m,300000m),
new Order("Germany",543456m,387345m)
};
A couple more declarations left that are specific to the ApexCharts library
private ApexChartOptions<Order> options { get; set; } = new();
private ApexChart<Order> chart;
Finally we focus on the HTML code by calling the ApexChart component with it's various parameters
@if (orders != null)
{
<ApexChart TItem="Order"
Title="Loading Sample Data to Display in Bar Chart"
Options="options"
@ref="chart">
<ApexPointSeries TItem="Order"
Items="orders"
SeriesType="SeriesType.Bar"
Name="Gross Value"
XValue="@(e => e.Country)"
YAggregate="@(e => e.Sum(e => e.GrossValue))"
OrderByDescending="e=>e.Y" />
</ApexChart>
}
else
{
@*SVG stands for Scalable Vector Graphics
SVG defines vector based graphics in XML format
This animation routine is taken from the Blazor ApexCharts site
and is used as a replacement for the typical loading gif animation
*@
}
Note the use of SVG (Scalable Vector Graphics) routine as a replacement for the usual loading gif technique
Other Graphs (Demo and Discuss)
ApexChartDemo2
horizontal bar graph
line graph
displaying Weather Data (FetchData) in a line graph
pie graph
line graph with mark click
<ApexChart TItem="Order"
Title="Loading Sample Data to Display in Line Graph with Marker Click"
Options="options"
OnMarkerClick="MarkerClick"
@ref="chart">
@if (selectedData != null)
{
<div class="alert-danger">You clicked @selectedData.DataPoint.X @selectedData.DataPoint.Items.Sum(e=> e.GrossValue).ToString("c") </div>
}
private SelectedData<Order> selectedData;
private void MarkerClick(SelectedData<Order> data)
{
selectedData = data;
}
line graph show data labels
<ApexPointSeries TItem="Order"
Items="orders"
Name="Gross Value"
SeriesType="SeriesType.Line"
XValue="@(e => e.Country)"
YAggregate="@(e => e.Sum(e => e.GrossValue))"
OrderByDescending="e=>e.Y"
ShowDataLabels />
pie graph with repositioned legend ... utilizing Options parameter
protected override async Task OnInitializedAsync()
{
options.Legend = new Legend { Position = LegendPosition.Top, FontSize = "20px", HorizontalAlign = Align.Center };
await LoadData();
}
pie graph toggles data point ... ref chart implementation
<button class="btn btn-primary" @onclick="ToggleDataPoint">Toggle Data Point</button>
private async Task ToggleDataPoint()
{
//Toggles 2nd country (0,1,2,3) Sorted in Descending Order
await chart.ToggleDataPointSelectionAsync(1, null);
}
Database Implementation (ApexChartDemo1DB)
Here we feed our chart data from a database using a call to an API controller
We also incorporate an Add/Edit Country Info pages to make the application even more dynamic
We display two series of data (GrossValue and NetValue) at the same time
@if (orders != null)
{
<ApexChart TItem="Order"
Title="Loading Sample Data to Display in Bar Chart"
Options="options"
@ref="chart">
<ApexPointSeries TItem="Order"
Items="orders"
SeriesType="SeriesType.Bar"
Name="Gross Value"
XValue="@(e => e.Country)"
YAggregate="@(e => e.Sum(e => e.GrossValue))"
OrderByDescending="e=>e.Y"
Color="#3633FF" />
<ApexPointSeries TItem="Order"
Items="orders"
SeriesType="SeriesType.Bar"
Name="Net Value"
XValue="@(e => e.Country)"
YAggregate="@(e => e.Sum(e => e.NetValue))"
OrderByDescending="e=>e.Y"
Color= "#E51C15"/>
</ApexChart>
}
Accessing a data service that implements the JSON API (WorldDemoLecture113)
Create a bar graph of country populations
Note: We only display countries whose population is greater than 50 million ... otherwise too many countries to display all at once.
Country[] countries { get; set; }
IList<Country> newcountries = new List<Country>();
rivate async Task LoadData()
{
var url = "http://outlier.oliversturm.com:8080/countries";
var client = HttpClientFactory.CreateClient();
var response = await client.GetAsync(url);
using (var responseStream = await response.Content.ReadAsStreamAsync())
{
var data = await JsonSerializer.DeserializeAsync<Data>(responseStream);
countries = data.data;
}
foreach (var item in countries)
{
if (item.population >50000000)
{
newcountries.Add(item);
}
}
}
<ApexPointSeries TItem="Country"
Items="newcountries"
SeriesType="SeriesType.Bar"
Name="Population"
XValue="@(e => e.name)"
YAggregate="@(e => e.Sum(e => e.population))"
OrderByDescending="e=>e.Y"
Color="#00ff00"/>
In the Lecture we will
Learn that Blazor is an excellent way to create web and mobile applications (Mobile apps with Blazor Hybrid ... Lectures coming soon). You can be super productive and build features fast. The result is a performant, stable application. But most applications need data. Many times it’s just a tiny amount of data, so a dedicated SQL server or even an Azure SQL instance is overkill for what you’re trying to accomplish. And possibly too expensive.
That’s where SQLite comes in. It’s a lightweight, disk-based database system that can be embedded in applications. You don’t need a database server to run SQLite. It’s just a file. Whether you know it or not, you use it every day. Mobile devices and enterprise applications all over the world use SQLite. It’s quick, easy, and lightweight.
SQLite reads and writes directly to ordinary disk files. A complete SQL database with multiple tables, indices, triggers, and views, is contained in a single disk file.
SQLite is generally a lot faster than MS SQL Server if dealing with small-size databases. SQLite can be integrated with different programming languages and environments including .NET.
Choosing MS SQL Server vs SQL Lite depends on the complexity of the application itself. For small embedded databases the best choice is SQLite to keep the application small. But for large-scale databases with multi-user access, the best choice is SQL Server.
Learn how to build a fully functional CRUD application in Blazor Server using Entity Framework Core and SQLite.
Create a straight forward application, that reads all the records from a Song table inside a Songs.db, SQLite database. This list of songs is then displayed in an HTML table on a user interface.
First we create a Blazor Server Application (BlazorServerSongSQLite) ... NET 7
Recall BlazorSongList4 (from Lecture 108-111) as a point of reference
Next we add the required NuGet Packages
Microsoft.EntityFrameworkCore.Sqlite (This allows Entity Framework Core to be used with SQLite.)
Microsoft.EntityFrameworkCore.Tools
Next we add a number of classes into the Data directory ... or Model folder (ie BlazorSongList app from Lecture 108)
We are using a model-first migration for Entity Framework. This means we create what we want the database to look like in code. Then Entity Framework Core will turn that into SQL and Databases
Song class
public class Song
{
public int Id { get; set; }
public string? Title { get; set; }
public string? Artist { get; set; }
public string? Year { get; set; }
}
SongDbContext (used during migration ... initial DB values are set during onModelCreating)
a database context is a fancy name for an object that controls access to the database. This object manages the life cycle of all the entities and generates SQL for you in the background.
public class SongDbContext: DbContext
{
public SongDbContext(DbContextOptions<SongDbContext> options)
: base(options)
{
}
public DbSet<Song> Song { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Song>().HasData(GetSongs());
base.OnModelCreating(modelBuilder);
}
private List<Song> GetSongs()
{
return new List<Song>
{
new Song { Id = 1001, Title="Thriller",Artist="Michael Jackson",Year="1980"},
new Song { Id = 1002, Title="Shower",Artist="James Taylor", Year="1973"}
};
}
}
SongServices (contains the CRUD operations)
public class SongServices
{
private SongDbContext dbContext;
public SongServices(SongDbContext dbContext)
{
this.dbContext = dbContext;
}
public async Task<List<Song>> GetSongAsync()
{
return await dbContext.Song.ToListAsync();
}
public async Task<Song> AddSongAsync(Song song)
{
try
{
dbContext.Song.Add(song);
await dbContext.SaveChangesAsync();
}
catch (Exception)
{
throw;
}
return song;
}
public async Task<Song> UpdateSongAsync(Song song)
{
try
{
var songExist = dbContext.Song.FirstOrDefault(p => p.Id == song.Id);
if (songExist != null)
{
dbContext.Update(song);
await dbContext.SaveChangesAsync();
}
}
catch (Exception)
{
throw;
}
return song;
}
public async Task DeleteSongAsync(Song song)
{
try
{
dbContext.Song.Remove(song);
await dbContext.SaveChangesAsync();
}
catch (Exception)
{
throw;
}
}
}
Now we add a using statement in _Imports.razor pointing to the Data folder (this saves us having to enter it on all subsequent Razor Component pages)
@using BlazorServerSongSQLite.Data
Next we register the SongDbContext and SongService in Program.cs (formerly placed in Startup.cs ... but no such file in .NET 6 onward)
//added for SQLite
builder.Services.AddDbContext<SongDbContext>(options =>
{
options.UseSqlite("Data Source=Songs.db");
}
);
builder.Services.AddScoped<SongServices>();
Note: we don't touch appsettings.json to point to the Songs database like we did in BlazorSongList
Create a new razor component page called SongsPage to add our user interface HTML code and C# logic
See CodeSnippet (SongsPageSnippet) for HTML code
@page "/songspage"
@inject SongServices service
<div class="container">
<div class="row bg-light">
<table class="table table-bordered table-hover">
<thead class="thead-dark">
<tr class="table-dark">
<th>Song Id</th>
<th>Title</th>
<th>Artist</th>
<th>Year</th>
<th>Delete Song</th>
</tr>
</thead>
<tbody>
@if (Songs.Any())
{
@foreach (var s in Songs)
{
<tr @onclick="(() => SetSongForUpdate(s))">
<td>@s.Id</td>
<td>@s.Title</td>
<td>@s.Artist</td>
<td>@s.Year</td>
<td><button class="btn btn-danger" @onclick="(() => DeleteSong(s))">Delete</button></td>
</tr>
}
}
else
{
<tr><td colspan="6"><strong>No songs available</strong></td></tr>
}
</tbody>
</table>
</div>
<div class="row m-5">
<div class="col-5 bg-light m-2 justify-content-start">
<div class="p-3 mb-1 bg-primary text-white text-center">Add New Song</div>
<div class="form-group ">
<label for="title">Song Title</label>
<input type="text" id="title" class="form-control" @bind-value="@NewSong.Title" />
</div>
<div class="form-group">
<label for="artist">Artist</label>
<input type="text" id="artist" class="form-control" @bind="@NewSong.Artist" />
</div>
<div class="form-group">
<label for="year">Year</label>
<input type="text" id="year" class="form-control" @bind="@NewSong.Year" />
</div>
<div class="text-center p-3 mb-3">
<button class="btn btn-info" @onclick="AddNewSong"> Add Song</button>
</div>
</div>
<div class="col-5 bg-light m-2 justify-content-end">
<div class="p-3 mb-1 bg-primary text-white text-center">Update Song</div>
<div class="form-group">
<label for="title">Song Title</label>
<input type="text" id="title" class="form-control" @bind-value="@UpdateSong.Title" />
</div>
<div class="form-group">
<label for="artist">Artist</label>
<input type="text" id="artist" class="form-control" @bind="@UpdateSong.Artist" />
</div>
<div class="form-group">
<label for="year">Year</label>
<input type="text" id="year" class="form-control" @bind="@UpdateSong.Year" />
</div>
<div class="text-center p-3 mb-3">
<button class="btn btn-info" @onclick="UpdateSongData"> Update Song</button>
</div>
</div>
</div>
</div>
@code {
List<Song> Songs = new List<Song>();
protected override async Task OnInitializedAsync()
{
await RefreshSongs();
}
private async Task RefreshSongs()
{
Songs = await service.GetSongAsync();
}
public Song NewSong { get; set; } = new Song();
private async Task AddNewSong()
{
if (NewSong.Artist != null && NewSong.Title!=null)
{
await service.AddSongAsync(NewSong);
NewSong = new Song();
}
await RefreshSongs();
}
Song UpdateSong = new Song();
private void SetSongForUpdate(Song song)
{
UpdateSong = song;
}
private async Task UpdateSongData()
{
await service.UpdateSongAsync(UpdateSong);
await RefreshSongs();
}
private async Task DeleteSong(Song song)
{
await service.DeleteSongAsync(song);
await RefreshSongs();
}
}
Add a little CSS code to the site.css in the wwwroot folder
tr:hover {
background-color:lightgray;
}
... And finally Add-Migration and Update-Database before we execute the application.
This adds our initial records to the Songs.db file
Learn how to use DB Browser for SQLite
A high quality, visual, open source tool to create, design, and edit database files compatible with SQLite.
View the contents of Songs.db ... just created during the Update-Database
Note: the Songs.db will also now be visible in the solution explorer
Highlight issue with Adding a new Song ... empty Name/Title ... and a simple solution
Offer you the Challenge to create another simple application of your choice to review and re-inforce the concepts covered in this lecture on using SQLite.
BlazorServerProdSQLite
Keeps track of Computer Store product inventory ... Product Id, Name, Price, Quantity, Description
Solution basically identical to Song application
Supplementary Demo
BlazorServerEmpSQLite
In this demo we build a fully functional CRUD (Create Read Update Delete) application in Blazor using Entity Framework Core and SQLite. We build an application to manage employees at a fictional company.
We track Employe Id, First Name, Last Name , Email, Department and Picture
For the most part the solution mirrors the steps used in the Song and Product applications with a couple of key differences ...
We start off creating a basic Blazor Server App
We install Entity Framework Core specifically for SQLite + Tools + Design
Using DB Browser for SQLite we create a blank database called Employees.db and store it in the Data folder
Now we create our classes: Employee.cs, EmployeeDbContext (check out code here slightly different than usual) and EmployeeService
public class EmployeeDbContext:DbContext
{
protected readonly IConfiguration Configuration;
public EmployeeDbContext(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite(Configuration.GetConnectionString("EmployeeDB"));
}
public DbSet<Employee> Employees { get; set; }
//Avatars from https://robohash.org/
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.ToTable("Employee");
modelBuilder.Entity<Employee>()
.HasData(
new Employee
{
Id = 1,
FirstName = "Tom",
LastName = "Smith",
Email = "smith@gmail.com",
Department = "Accounting",
Avatar = "https://robohash.org/5RS.png?set=set2&size=150x150"
}
);
}
}
Next we connect to our SQLite datbase by reference it in appsettings.json ..... ConnectionStrings": { "EmployeeDB": "Data Source=Data\\Employees.db"}
Next we register EmployeeDbContext and EmployeeService in Program.cs ... again check out code for small difference
builder.Services.AddDbContextFactory<EmployeeDbContext>(options =>
options.UseSqlite(connectionString));
Finally we are ready to perform our migration and update the database
The last stage is now focusing on CRUD ...
We create a new Razor Component called EmployeesPage where we will use code behind (partial class)
Note how we use Dependency Injection in the code behind file
public partial class EmployeesPage
{
[Inject]
public EmployeeService service { get; set; }
public bool ShowCreate { get; set; }
public bool EditRecord { get; set; }
public Employee? NewEmployee { get;set; }
public Employee? EmployeeToUpdate { get; set; }
public List<Employee>? TheEmployees { get; set; }
protected override async Task OnInitializedAsync()
{
ShowCreate = false;
await ShowEmployees();
}
public async Task ShowEmployees()
{
TheEmployees= await service.GetEmployeeAsync();
}
public void ShowCreateForm()
{
NewEmployee= new Employee();
ShowCreate = true;
}
public async Task CreateNewEmployee()
{
if (NewEmployee.FirstName!=null && NewEmployee.LastName!=null)
{
await service.AddEmployeeAsync(NewEmployee);
}
ShowCreate = false;
await ShowEmployees();
}
public async Task ShowEditForm(Employee theEmployee)
{
EditRecord = true;
EmployeeToUpdate = TheEmployees.FirstOrDefault(p => p.Id == theEmployee.Id);
}
public async Task UpdateEmployee()
{
EditRecord = false;
if (EmployeeToUpdate is not null)
{
await service.UpdateEmployeeAsync(EmployeeToUpdate);
await ShowEmployees();
}
}
public async Task DeleteEmployee(Employee theEmployee)
{
if (theEmployee is not null)
{
await service.DeleteEmployeeAsync(theEmployee);
await ShowEmployees();
}
}
}
Note ... all the UI happens on the EmployeesPage.razor (Create/Read/Update/Delete) . No other components are used. We use two bool values to display/hide Create and Edit forms ... note the use of Editing in Place
The images are from https://robohash.org/
In this Lecture we will
Learn that using Blazor for client-side Web UI with .NET is a fantastic solution, but sometimes full access to the native capabilities of the device is required and out of reach of Blazor on the Web (Server/WebAssembly) . Blazor Hybrid expands beyond the web and combines Web technologies (HTML, CSS, and optionally JavaScript) with native capabilities in .NET MAUI Blazor.
Learn that Blazor Hybrid (.NET MAUI Blazor) is both a Windows Desktop (a step above PWA) and Mobile creating application. With Blazor Hybrid, the primary goal has shifted slightly by extending the capabilities of .NET developers beyond the Web into desktop and mobile development.... BUT it can also be modified by adding a Blazor Server or WebAssembly project to become a Web Application.
Although Blazor PWA apps can easily be created, there are tradeoffs. Because there's no .NET API support for service workers, all functionalities must be done in JavaScript. And because one of Blazor's attractions is Csharp, this deters some developers from venturing too deep into service workers.
In a Blazor Hybrid app, Razor components run natively on the device. So we have a Blazor Web UI rendered in a Native Andriod,iOS, MacOS or Windows Application.Components render to an embedded Web View control (BlazorWebView) through a local interop channel. Components don't run in the browser, and WebAssembly isn't involved. Razor components load and execute code quickly, and components have full access to the native capabilities of the device through the .NET platform. The .NET platform provides BlazorWebView controls for the specific native UI framework you use in your application. So, you have a BlazorWebView for .NET MAUI, one for WPF, and one for Windows Forms etc.
Learn that MAUI (proper ... not .NET MAUI Blazor/Blazor Hybrid) stands for Multi-Platform App UI
It is a cross-platform framework for creating native mobile and desktop apps with Csharp and XAML. Using .NET MAUI, you can develop apps that can run on Android, iOS, macOS, and Windows from a single shared code-base. One of the key aims of .NET MAUI is to enable you to implement as much of your app logic and UI layout as possible in a single code-base. NET MAUI unifies Android, iOS, macOS, and Windows APIs into a single API that allows a write-once run-anywhere developer experience, while additionally providing deep access to every aspect of each native platform.
Setup Requirements
Visual Studio modifications ... things to possibly add
ASP.NET and Web Development
.NET Desktop Development
Mobile Development with .NET
Learn how to activate Hyper V to speed up Android Emulator use and other tweaks
You may need to enable developer mode to run the application in Windows Machine.
... What about MAC computer? ... Must actually have MAC computer to emulate screen on Windows machine.
Create a Simple Todo Application which creates a Desktop, Mobile and Web Application.
HybridTodoLibNET8updated
Start off by creating a Project ... .NET MAUI Blazor Hybrid App ... Use .NET 8 (VS 2022) or greater (VS 2026) .
The .NET 8 demos in the Resources will run in Visual Studio 2022 but will not work using Visual Studio 2026 ... look for selected updated 2026 versions
Make note of the new folders and files in the Solution ... Platforms/Resources (MAUI specific folders) and files in particular MauiProgram.cs and MainPage.xaml
In the MainPage.xaml you can see the first introduction of Blazor as a BlazorWebView. This is the component that allows your Blazor UI to render natively on your MAUI application.The MainPage wraps the BlazorWebView directly within a ContentPage, essentially creating a full-page Blazor view inside of the application's UI shell.
XAML, stands for eXtensible Application Markup Language, and is Microsoft's variant of XML for describing a GUI. In previous GUI frameworks, like WinForms, a GUI was created in the same language that you would use for interacting with the GUI, e.g. Csharp or VB.NET and usually maintained by the designer (e.g. Visual Studio), but with XAML, Microsoft is going another way. Much like with HTML, you are able to easily write and edit your GUI.
XAML is used when creating the UI for WPF apps. Windows Presentation Foundation (WPF) is another UI framework that creates desktop client applications.
Run the app as is in the two primary modes available ... Windows Machine and using the Andriod Emulator (Mobile app)
Note that a Web App is not available by default but we can easily add a Server/WASM Project and by doing so create a Web App ... we will do this at the end of this lecture
Now lets create a simple Todo App (Recall Lecture 98 and Lectures 140-142) ... but not in the Hybrid project or Server/WASM Project.
As we will learn in this lecture,by leveraging the power of a Razor Class Library we will be able to easily add it to the Server Project and the Hybrid Project and reduce repetitive coding
Add a new Project ... Razor Class Library (call it RazorLibrary). Only keep wwwroot and _Imports.razor
Let's create our Todo class first by making a folder called Models and naming our class TodoModel
public class TodoModel
{
public string TodoItem { get; set; }
public bool IsComplete { get; set; }
}
Next go into _Imports.razor and add a reference to the Models folder and reference to Forms (necessary when we use EditForm in the Razor Component)
@using RazorLibrary.Models
@using Microsoft.AspNetCore.Components.Forms
Now let's create our Razor component ... called it TodoComponent
First ,as usual I like to add the code section
private List<TodoModel> todos = new();
private TodoModel todo = new();
private void AddTodo()
{
todos.Add(todo);
todo = new();
}
Now we go up into the HTML
First we define an area to accept Todos
<EditForm Model="@todo">
<div class="mb-3">
<label for="todoItem" class="form-label">ToDo Item:</label>
<InputText @bind="@todo.TodoItem" class="form-control" id="todoItem" />
<button class="btn btn-primary" @onclick="AddTodo">Add</button>
</div>
</EditForm>
Then we loop through all the current Todos and add a button beside each one to be used when the Todo is DONE
<h3>ToDo List</h3>
<ul>
@foreach(var t in todos)
{
<li class="mb-2">
@if(t.IsComplete)
{
<span style="text-decoration:line-through">
@t.TodoItem
</span>
}
else
{
@t.TodoItem
<button class="btn btn-warning btn-sm ms-3"
@onclick="@(() => t.IsComplete=true)">
Complete
</button>
}
</li>
}
</ul>
Now we are ready to implement our Component in our Project
Go to Dependencies and add the Project Reference to RazorLibrary
add @using RazorLibrary to _Imports.razor files
Finally add a reference to the TodoComponent in the Index.razor page.
Now let's create a Server Project called BlazorServer / Blazor WebAssembly Standalone App (NET 8) to show you how you can easily implement a Web App within this Hybrid App
Go to Dependencies and add the Project Reference to RazorLibrary
add @using RazorLibrary to _Imports.razor files
Finally add a reference to the TodoComponent in the Index.razor page.
@page "/"
<PageTitle>Index</PageTitle>
<h1>Blazor Server Web Application within Hybrid App</h1>
<TodoComponent/>
Before executing the application set the Startup Project to BlazorServer/WASM
... Now sharing components from a library won't always work if you are accessing special features for instance specific to a mobile app lets say on an Andriod or IOS device.
Supplementary Demos
HybridEmailLibNET8
Nice review and extension of the concepts covered in this Lecture (implementing a Razor Class Library)
Note the use of Multiple Startup Projects (Server/Web App and Hybrid/Windows/Mobile App) ... Right Click on Solution and choose "Configure Startup Projects"
Note the use of the different Popup (.NET MAUI Alert Popup or Javascript) depending on whether you are viewing the Windows Machine App/Mobile (Client/Hybrid Project) or the Web App (Server Project)
CustomerComponent.razor (created in Razor Class Library)
@code {
//Used to deal with different ways to display pop-ups in
//Blazor Hybrid (.NET MAUI Blazor) vs Blazor Server/WASM browser apps
//... In Hybrid we have Application.Current.MainPage.DisplayAlert
//... In Server/WASM we will need to use Javascript Alert
[Parameter]
public EventCallback ShowAlert{ get; set; }
public class Customer
{
public string? Name{ get; set; }
public string? Email { get; set; }
}
private List<Customer> customers = new();
private Customer model = new();
private string spinnerClass = "";
private async void AddCustomer()
{
if ((string.IsNullOrWhiteSpace(model.Name) || string.IsNullOrWhiteSpace(model.Email)))
{
spinnerClass = "";
}
else
{
spinnerClass = "spinner-border spinner-border-sm";
await Task.Delay(2000);
spinnerClass = "";
customers.Add(model);
model = new();
StateHasChanged();
}
}
}
Key notes in HTML section
<div class="col-md-4 mb-3">
<button class="btn btn-primary" @onclick="AddCustomer">
<span class="@spinnerClass" role="status" aria-hidden="true"> </span>
Add Customer
</button>
</div>
<div class="col-md-4 mb-3">
<button class="btn btn-primary" @onclick="ShowAlert">Show Alert</button>
</div>
In the HybridEmailLib Project ... Index.razor page
@page "/"
<CustomerComponent ShowAlert="HybridAlert"/>
@code {
private async Task HybridAlert()
{
await Application.Current.MainPage.DisplayAlert("Hybrid Alert", "Welcome to .NET MAUI Blazor", "OK");
}
}
In the BlazorServerApp Project ... Index.razor.page
@page "/"
@inject IJSRuntime js
<PageTitle>Index</PageTitle>
<CustomerComponent ShowAlert="ServerAlert"/>
@code {
private async Task ServerAlert()
{
await js.InvokeVoidAsync("alert", "Server App within .NET MAUI Blazor ... using Javascript Alert");
}
}
BlazorHybridFontsNET8
In this simple application we change the Body Font to Poppins and Highlight a number of Free Resources you can use when developing your Apps.
The 'Poppins' font and many others can be found at Google Fonts
Once you download the Font Family ... you will have a directory with all the True Type Fonts associated with this Family
Next we create a sub folder called fonts in the css folder since the style sheet will implement this font across our App
Now just drag the particular font style you wish to implement to this fonts folder (Poppins-Bold.tff/Poppins-Regular.tff)
The last step is to modify the app.css file to use this new Font Family ... check out the code in app.css
@font-face {
font-family: 'Poppins';
src: url('./fonts/Poppins-Regular.ttf') format('truetype');
}
html, body {
/*font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;*/
font-family: 'Poppins', Arial, Helvetica, sans-serif,Arial,sans-serif;
}
Image Sources ... icons, stickers, royalty free images
Check out the Index , Counter and Fetch Data page for examples of the possible images available
All images at stored in the images folder in the wwwroot folder
In this Lecture we will
Create a simple Blazor Hybrid Application (HybridCheckInternetNET8) that accesses cross platform APIs that are built in .NET MAUI Blazor to determine if a user has an active internet connection and the type (Wifi/Ethernet)
Create a new Project .NET MAUI Blazor... pick .NET 8 this time
To demonstrate accessing a cross platform API we will go into the Counter page and add a new button
<button class="btn btn-primary" @onclick="CheckInternet">Check Internet</button>
Now we go into the code section
first thing we need to do is gain access to those APIs using the command
@using Microsoft.Maui.Networking
private async void CheckInternet()
{
var hasInternet = Microsoft.Maui.Networking.Connectivity.Current.NetworkAccess ==
NetworkAccess.Internet;
var internetType = Connectivity.Current.ConnectionProfiles.FirstOrDefault();
//Here we are using a native pop-up ... new!
//It will look different depending on whether you are running
//a Desktop Windows app or Mobile app
await Application.Current.MainPage.DisplayAlert("Internet", "Status: " + hasInternet + " of type " + internetType, "OK");
}
Note: DisplayAlert and other Notification techniques are re-visited in next lecture
Run the app and see the results
Now lets take a look how to mix and match the Webview Web components and the Native components ... making it a little more native
Go into MainPage.xaml and we will try to modify the code to use native Tabs
Change from ContentPage to <TabbedPage and remove the background
Wrap the BlazorWebView in a <ContentPage Title="Home" Padding="10"> ... make two more copies for Counter and Weather
Now go to each ContentPage and change each to point to the corresponding razor component page
ComponentType="{x:Type pages:Index}"
Make sure to add
x:Class="HybridCheckInternet.MainPage"
xmlns:pages="clr-namespace:HybridCheckInternet.Pages">
<TabbedPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:HybridTodoNET8"
xmlns:pages="clr-namespace:HybridTodoNET8.Components.Pages"
x:Class="HybridTodoNET8.MainPage" >
<ContentPage Title="Home">
<BlazorWebView HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type pages:Home}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
</ContentPage>
<ContentPage Title="Counter">
<BlazorWebView HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type pages:Counter}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
</ContentPage>
<ContentPage Title="Weather">
<BlazorWebView HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type pages:Weather}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
</ContentPage>
<ContentPage Title="ToDo">
<BlazorWebView HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type pages:Todo}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
</ContentPage>
<pages:NewPage1 Title="Native"/>
</TabbedPage>
Now go to the code behind for MainPage.xaml.cs and change ContentPage to TabbedPage
public partial class MainPage : TabbedPage
{
public MainPage()
{
InitializeComponent();
}
}
Now lets go into the Pages folder and add a Native .MAUI page not a basic Razor Component Page
Add a new item ... .NET MAUI ContentPage (XAML) use default name NewPage1.xaml and add a couple of controls
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="HybridCheckInternet.Pages.NewPage1"
Title="Chiarelli">
<VerticalStackLayout>
<Label
Text="Welcome to .NET MAUI!"
TextColor="Red"
VerticalOptions="Center"
HorizontalOptions="Center" />
<Button Text="Native Button" Clicked="OnButtonClicked"/>
<CheckBox/>
<Switch/>
</VerticalStackLayout>
</ContentPage>
NewPage1.xaml.cs
async void OnButtonClicked(object sender, EventArgs args)
{
Button button = (Button)sender;
await DisplayAlert("Clicked!",
"The button labeled '" + button.Text + "' has been clicked",
"OK");
}
You can now sneak this new Page into the MainPage.xaml just above the </TabbedPage>
<pages:NewPage1/>
Now when we run this we should get a bunch of different tabs within the app in either Desktop or Mobile View.
.... but make sure to remove x:Name="blazorWebView" from all the ContentPage components in MainPage.xaml
<BlazorWebView HostPage="wwwroot/index.html">
Check out the Updated versions
HybridCheckInternetUpdFinalTooltipNET8
UPDATE ... To Native Page (Chiarelli)
We added A Text to Speech command that asks to Pick an Image
We added an Image Control
We implement a FilePicker which allows you to only pick images ... once the image is picked it is placed in the image control on screen
We have the Switch respond to a state change (Toggled) by making the loaded in image appear and disappear
Checkbox changes (CheckedChanged) Title to 'Updated' and then back to original
Added ...VerticalOptions="Center" ...HorizontalOptions="Center" to VerticalStack and BackgroundColor to ContentPage to tweak appearance somewhat
Added a new button to Close MAUI page if called Modally from Counter page ... does not work if page is simply accessed via Tabbed Menu
Associated code behind for button uses command ... Navigation.PopModalAsync();
NewPage1.xaml
<VerticalStackLayout
VerticalOptions="Center"
HorizontalOptions="Center"
Spacing="10">
<Label
x:Name="myLabel"
Text="Welcome to .NET MAUI!"
TextColor="Red"
VerticalOptions="Center"
HorizontalOptions="Center" />
<Button Text="Native Button ... Click to Display File Picker ... Images only " Clicked="OnButtonClicked"/>
<CheckBox CheckedChanged="OnCheckBoxCheckedChanged"/>
<Switch OnColor="Orange" ThumbColor="Green" IsToggled="True" Toggled="OnToggled" />
<Image
x:Name="myImage"
Source="dotnet_bot.png"
SemanticProperties.Description="Test"
HeightRequest="200"
HorizontalOptions="Center"/>
<Button Text="Close ... Use only when this MAUI Page is called from Razor Component Page" Clicked="Button_Close"/>
</VerticalStackLayout>
NewPage1.xaml.cs
async void OnButtonClicked(object sender, EventArgs args)
{
Button button = (Button)sender;
await DisplayAlert("Clicked!",
"The button labeled '" + button.Text + "' has been clicked",
"OK");
await TextToSpeech.Default.SpeakAsync("Pick an Image please");
var result = await FilePicker.PickAsync(new PickOptions
{
PickerTitle = "Pick image please",
FileTypes = FilePickerFileType.Images
});
if (result==null)
{
return;
}
else
{
var stream = await result.OpenReadAsync();
myImage.Source=ImageSource.FromStream( () => stream );
}
}
void OnToggled(object sender, ToggledEventArgs e)
{
if(e.Value==true)
{
myImage.IsVisible = true;
}
else
{
myImage.IsVisible = false;
}
}
void OnCheckBoxCheckedChanged(object sender,CheckedChangedEventArgs e)
{
if (e.Value==true)
{
myLabel.Text = "Welcome to .NET MAUI Updated! ";
}
else
{
myLabel.Text = "Welcome to.NET MAUI!";
}
}
private void Button_Close(object sender, EventArgs e)
{
Navigation.PopModalAsync();
}
UPDATE ... TO Counter Page
New button added to demonstrate how to navigate to a .NET MAUI page from a Razor Component Page
App.Current.MainPage.Navigation.PushModalAsync(new Pages.NewPage1());
HybridCheckInternetDropDownMenuNET8
UPDATE ... Drop Down Menu instead of TabbedPage ... Notice the Content on the top left
Idea from Fritz on the Web
The menu <MenuFlyoutItem> is created in App.xaml (using a single BlazorView component within a Shell) instead of MainPage.xaml
<Shell Title="Drop Down Menu">
<ShellContent>
<ContentPage>
<ContentPage.MenuBarItems>
<MenuBarItem Text="Content">
<MenuFlyoutItem Text="Home" Clicked="MenuItem_Clicked"></MenuFlyoutItem>
<MenuFlyoutItem Text="Counter" Clicked="MenuItem_Clicked"></MenuFlyoutItem>
<MenuFlyoutItem Text="Weather" Clicked="MenuItem_Clicked"></MenuFlyoutItem>
<MenuFlyoutItem Text="Chiarelli" Clicked="MenuItem_Clicked"></MenuFlyoutItem>
</MenuBarItem>
</ContentPage.MenuBarItems>
<BlazorWebView x:Name="blazorWebView1" HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="ComponentType="{x:Type local:Components.Routes}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
</ContentPage>
</ShellContent>
Note in App.xaml.cs MainPage = new MainPage() is removed since we are specifying our own MainPage inside the XAML markup
public NavigatorService NavigatorService { get; }
public App(NavigatorService navigatorService)
{
InitializeComponent();
NavigatorService = navigatorService;
//MainPage = new MainPage();
}
private async void MenuItem_Clicked(object sender, EventArgs e)
{
var menuItem = (MenuItem)sender;
var url = menuItem.Text switch
{
"Counter" => "counter",
"Weather" => "fetchdata",
"Chiarelli" => "NewPage1",
_ => "/"
};
await Application.Current.MainPage.DisplayAlert("You chose the "+ menuItem.Text + " page ","Status","OK");
if (url=="NewPage1")
{
//Navigating to a xaml page from a xaml page .NET MAUI
App.Current.MainPage.Navigation.PushModalAsync(new Pages.NewPage1());
}
else
{
//Navigating to a razor page from a xaml page .NET MAUI
NavigatorService.NavigationManager.NavigateTo(url);
}
}
Each menu item triggers the Clicked event where we will execute the same async method MenuItem_Clicked ... only difference will be the location they target
*** Key Concept ***
Notice in my second update how we were able to Navigate from the Counter page (Blazor razor component) to the XAML page (.NET MAUI) ... BUT we need to now do the reverse ... go from a XAML page to a Blazor razor page.
The Blazor NavigationManager is not directly accesible in .NET MAUI. We need to create a service that will allow us to capture the NavigationManager and interact with it.
I created a folder called Data and created a simple class called NavigatorService ... with one property public required NavigationManager NavigationManager {get;set;} ... I added a using pointing to this folder in _Imports.razor
In MauiProgram.cs we registered this class/service
builder.Services.AddSingleton<NavigatorService>();
Next ... in the MainLayout we comment out or remove the original side bar and then inject the NavigatorService and NavigationManager ... this is where we will capture the NavigationManager and interact with it (See OnInitialized) ... now it will be available on all our pages.
@inherits LayoutComponentBase
@inject NavigatorService NavigationService
@inject NavigationManager NavigationManager
<div class="page">
@* <div class="sidebar">
<NavMenu />
</div> *@
<main>
@* <div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div> *@
<article class="content px-4">
@Body
</article>
</main>
</div>
@code {
protected override void OnInitialized()
{
NavigationService.NavigationManager = NavigationManager;
//base.OnInitialized();
}
}
Finally we add the NavigatorService to App.xaml.cs so that it is injected and stored as a property for use later
Lastly in the App.xaml.cs we flesh out our MenuItem_Clicked method using a switch command to determine which menu item was picked and then NavigateTo the requested page.
Suggested Exercise
Redo the LocalWeatherForecast PWA from Lectures 147-149 (LocalWeatherForecastLect149) and create a Windows Desktop App version
Extra Help Resources
Check out the External Resource ... Going Native with Blazor
HybridGeoLocationNET8 (no Javascript necessary )
@using Microsoft.Maui.Devices.Sensors
<h5>Location: Latitude @location?.Latitude ... Longitude @location?.Longitude</h5>
@code {
private Location location;
protected override async Task OnInitializedAsync()
{
location = await Geolocation.GetLocationAsync();
}
}
HybridGeoLocationAndroid
For Android Implementation you must edit the AndroidManifest.xml file in Platforms/Android folder
Required Permissions
Access_Coarse_Location
Access_Fine_Location
Note: In Visual Studio 2026 the Manifest Editor is only available with .NET 9 or higher
HybdWeatherNET8/HybdWeatherAndroid
Requires OpenWeather Subscription to access data (circa 2024)
Note the use of Newtonsoft.Json Nuget Package to Deserialize api data
//idea from https://www.youtube.com/watch?v=Br_VfpKAbvI
string json = await client.GetStringAsync(url.ToString());
forecast = JsonConvert.DeserializeObject<OpenWeatherForecast>(json);
//forecast = await Http.GetFromJsonAsync<OpenWeatherForecast>(url.ToString());
In this Lecture we will
Again create a simple Todo Application (HybridTodoLocalSaveJSONNET8) with the extra feature of saving our data as a JSON file locally (runs in Windows Desktop and Andriod Mobile ... NOT a DB app). Recall Lectures 98 and Lectures 140-142
Start off and create a .NET MAUI Blazor project
Now in the already existing Data folder create the TodoItem class
public class TodoItem
{
public string? Title { get; set; }
public bool IsDone { get; set; } = false;
}
Next create a skelton version of the Todo.razor page and create a link to it from the NavMenu
Now lets flesh out the code and HTML for our Todo page ... very similiar to previous Todo apps covered already.
private List<TodoItem> todos = new();
private string? newTodo;
private void AddToDo()
{
if (!string.IsNullOrWhiteSpace(newTodo))
{
todos.Add(new TodoItem { Title = newTodo });
newTodo = string.Empty;
}
}
@page "/todo"
@using HybridTodoLocalSaveJSON.Data
<h3>To Do List</h3>
<h3>Todo (@todos.Count(t => !t.IsDone))</h3>
<br />
<br />
<input placeholder="Something to do" @bind="newTodo" />
<button @onclick="AddToDo">Add todo</button>
<ul class="list-unstyled">
@foreach (var todo in todos)
{
<li>
<input type="checkbox" @bind="todo.IsDone" />
<input @bind="todo.Title" class="@(todo.IsDone? "alert-danger":"")" />
</li>
}
</ul>
Let's finish off this Todo Application by looking at how to access some platform features in Blazor ... specifically loading and saving locally in JSON format.
Let's start by adding two new <button> elements for our load and save button under the <h3> and above our list of todo items.
<button @onclick="Save">Save</button>
<button @onclick="Load">Load</button>
... and lets add the corresponding methods (empty right now)
private async Task Save()
{
}
private async Task Load()
{
}
.NET includes the System.IO namespace that includes the ability to load and save files to disk. .NET MAUI maps this functionality to native APIs for you automatically; all you need to do is specify where to save the file. Each platform has special locations to save user data. The file system helpers in .NET MAUI provide access to get multiple platform directories including the cache and app data directories. It also can load files that are bundled directly into the app. Now, let's implement the Save method by using System.Text.Json, which is built into .NET.
Add the using directive for Microsoft.Maui.Storage, System.Text.Json, and System.IO.
@using Microsoft.Maui.Storage
@using System.IO
@using System.Text.Json
... and now complete the Save and Load methods.
private async Task Save()
{
//Here we serialize the data into a string, create the path for the file
//and write the contents.
var contents = JsonSerializer.Serialize(todos);
var path = Path.Combine(FileSystem.AppDataDirectory, "todo.json");
File.WriteAllText(path, contents);
await App.Current.MainPage.DisplayAlert("List Saved ", "List has been save to " + path, "OK");
}
private async Task Load()
{
//Here we deserialize the data and load the items into the todo list
var path = Path.Combine(FileSystem.AppDataDirectory, "todo.json");
if (!File.Exists(path))
return;
var contents = File.ReadAllText(path);
var savedItems = JsonSerializer.Deserialize<List<TodoItem>>(contents);
todos.AddRange(savedItems); //used to add multiple elements all at once to the list
}
Check out the updated versions
HybridTodoLocalSaveJSONPreferencesNET8
An update to the first demo of this Lecture that uses the Preferences Class methods Set and Get to store application preferences in a key/value format
@code {
private string lastUsedTimeStamp;
private void WriteToStorage()
{
lastUsedTimeStamp = DateTime.Now.ToString();
Preferences.Set("Last_Used", lastUsedTimeStamp);
}
private void ReadFromStorage()
{
lastUsedTimeStamp = Preferences.Get("Last_Used", "default_value");
}
}
HybridTodoServiceLocalSaveJSONnet8 (uses a Service for Saving and Loading Json file)
TodoService.cs
public class TodoService
{
//This service needs to be declared in MauiProgram.cs
//so that it can be injected on our Todo.razor page
string file = string.Empty;
//Constructor
public TodoService()
{
file = Path.Combine(FileSystem.AppDataDirectory, "items.json");
}
public void SaveItems(IEnumerable<TodoItem> items)
{
//using System.IO
File.WriteAllText(file, JsonSerializer.Serialize(items));
}
public IEnumerable<TodoItem> GetItems()
{
if (!File.Exists(file))
return Enumerable.Empty<TodoItem>();
var itemJson = File.ReadAllText(file);
return JsonSerializer.Deserialize<IEnumerable<TodoItem>>(itemJson) ?? Enumerable.Empty<TodoItem>();
}
}
Todo.razor
@code {
List<TodoItem> todos = new List<TodoItem>();
string newTodo;
protected override void OnInitialized()
{
var items = TodoService.GetItems();
todos.AddRange(items);//used to add multiple elements all at once to the list
}
private void AddTodo()
{
if (!string.IsNullOrWhiteSpace(newTodo))
{
TodoItem a = new TodoItem();
a.Title = newTodo;
todos.Add(a);
newTodo = string.Empty;
}
}
private void Save()
{
TodoService.SaveItems(todos);
}
}
HTML section
@page "/todo"
@inject TodoService TodoService
<ul class="list-unstyled">
@foreach (var todo in todos)
{
<li>
<input type="checkbox" @bind="todo.IsDone"/>
<span style = "@(todo.IsDone ? "text-decoration:line-through; color:red":"")">@todo.Title</span>
</li>
}
</ul>
<input placeholder="Add a To Do" @bind="newTodo"/>
<button class="btn btn-primary" @onclick="AddTodo">Add Todo</button>
<img src="images/click.gif" width="30" height="30" />
<button class="btn btn-secondary" @onclick="Save">Save</button>
HybridTodoNativeAndWebUInet8
This example updates the previous example where we used a Service. In this version we mix and match Native (XAML) and Windows Desktop UI (BlazorView) components. The BlazorView contents appears at the top and the Native button appears along the bottom.
MainPage.xaml
Create grid with 2 rows. Height of 2nd row distributed evenly based on size of content
<Grid RowDefinitions="*,Auto">
<BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type local:Components.Routes}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
<Button Text="Native Button"
Margin="10"
Grid.Row="1"
Clicked="Button_Clicked"/>
</Grid>
MainPage.xaml.cs
private async void Button_Clicked(object sender, EventArgs e)
{
await DisplayAlert("Native UI", "Coming from Native Button", "OK");
}
HybridTodoGridLayoutNET8
In this version we integrate our 4 Blazor Components plus a .NET MAUI (XAML) Native Button into the .NET MAUI Grid Layout (on screen all at once) within a Blazor Hybrid App.
MainPage.xaml
Grid Margin="40" RowSpacing="10" ColumnSpacing="10">
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="75" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label
Text="Seamlessly Add Blazor UI Components and .NET MAUI (XAML) Native Components in Blazor Hybrid Apps"
TextColor="White"
TextDecorations="Underline"
HorizontalOptions="Center"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2" />
<BlazorWebView Grid.Row="1" Grid.Column="0" HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type pages:Home}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
<BlazorWebView Grid.Row="1" Grid.Column="1" HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type pages:Todo}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
<BlazorWebView Grid.Row="2" Grid.Column="0" HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type pages:Counter}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
<BlazorWebView Grid.Row="2" Grid.Column="1" HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type pages:Weather}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
<Button Text="Native Button"
BackgroundColor="Red"
Margin="10"
Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="2"
Clicked="Button_Clicked"/>
</Grid>
private async void Button_Clicked(object sender, EventArgs e)
{
await DisplayAlert("Native UI", "This is a .NET MAUI (XAML) Button", "OK");
await TextToSpeech.Default.SpeakAsync("Notice here how we integrated our 4 Blazor Components <b>plus</b> a NET MAUI Native Button into the NET MAUI Grid Layout within a Blazor Hybrid App");
}
Note ... to make each Page standout a background color has been added
@page "/counter"
<style>
body {
background-color: greenyellow;
}
</style>
<h1>Counter</h1>
HybridTodoTabbedMenuNET8
We remove the side Blazor menu and use a .NET MAUI native Tabbbed Menu
Similar to previous Lecture HybridCheckInternetNET8
Learn how to display Notifications and Edit Local Images/Text from a .NET MAUI Blazor Hybrid App
Demo only ... HybridTextImages
Notification techniques (notice similarity with Javascript ... Alert/Confirm/Prompt)
DisplayAlert/DisplayAlert (with confirm) ... note the use of the ternary conditional operator
private async Task ShowAlert()
{
await Application.Current.MainPage.DisplayAlert("Alert", "Welcome", "OK");
}
private async Task ShowConfirm()
{
var result = await Application.Current.MainPage.DisplayAlert(" Testing new type of Alert Confirm", "Are you sure...?", "Yes", "No");
await Application.Current.MainPage.DisplayAlert("Alert", "You replied " + (result ? "Yes":"No"), "OK");
}
DisplayActionSheet
private async Task ShowActionSheet()
{
//Here we set the destroy button to null ... could be set to Delete for instance
var result = await Application.Current.MainPage.DisplayActionSheet("Which Platform do you use", "Cancel", null, "Twitter", "Linkedin","Instagram","TikTok");
await Application.Current.MainPage.DisplayAlert("ActionSheet", "You chose " + result, "OK");
}
DisplayPromptAsync (text/numbers)
private async Task ShowPrompt()
{
var result = await Application.Current.MainPage.DisplayPromptAsync("Welcome","Please enter your Name");
await Application.Current.MainPage.DisplayAlert("Response", "Hello " + result, "OK");
var resultAge = await Application.Current.MainPage.DisplayPromptAsync("Info", "Please enter your age", initialValue: "10", maxLength: 3, keyboard: Keyboard.Numeric);
await Application.Current.MainPage.DisplayAlert("Response", "Your age has been registered as " + resultAge, "OK");
}
The key to accessing the local resources is the System.Diagnostics.Process.Start method,
which is used here to display text from Notepad and images in MS Paint right in the native client application (in this case Windows Desktop App)... does not work in Andriod Mobile App or a Web App.
Opening the text editor of your choice is simple. If it's in the System32 folder, you just need:
void GetMessage()
{
var file = Path.GetTempFileName(); //defaults to : C:\\Users\\chiar\\AppData\\Local\\Temp\\tmpBC69.tmp
File.WriteAllText(file, message);
var p = System.Diagnostics.Process.Start("notepad", file);
p.WaitForExit();
message = File.ReadAllText(file);
}
If you want to use the third-party Notepad++ that's installed on your machine, it would be:
System.Diagnostics.Process.Start(@"C:\Program Files\Notepad++\notepad++.exe");
Note how we reference the location of the image located in the wwwroot\images folder
private void EditImage()
{
string rootpath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wwwroot");
var file = rootpath + @"\images\blazor.png";
System.Diagnostics.Process.Start("mspaint",file);
}
In this Lecture we will
Create a Blazor Hybrid app (HybridSQLiteSongNET8) with full CRUD abilities that implements an SQLite database. (Recall Lecture 165 SongSQLite ... BlazorServerSongSQLite)
SQLite-net is shipped as a NuGet package. You must add the sqlite-net-pcl package to your apps to use it. Use the NuGet package manager in Visual Studio. Additionally, if you want to run an app on Android, you may also need to add the SQLitePCLRaw.provider.dynamic_cdecl package
Using .NET 8
install the Nuget Package sqlite-net-pcl
install the Nuget Package SQLitePCLRaw.provider.dynamic_cdecl package (required for Andriod)
install the Nuget Package SQLitePCLRaw.bundle_green (needed in .NET 8 ... exposes SQLite on each platform)
install the Nuget Package SQLitePCLRaw.provider.sqlite3 (helps with IOS)
Start off and make a folder called Models and create the Song class
SQLite is an object relational mapper , which means you can build your database schema from Csharp classes. SQLite can build a database table from an ordinary Csharp class.
public class Song {
[PrimaryKey, AutoIncrement]
public int SongId { get; set; }
public string Title { get; set; }
public string Artist { get; set; }
public string Year { get; set; }
public string Gender { get; set; }
}
Next we create a folder called Services where we will place our Interface (ISongService) and the class/service (SongService) which implements it.
ISongService
add the usings first
using HybridSongSQLite.Models;
using SQLite;
public interface ISongService
{
Task<List<Song>> GetAllSongs();
Task<Song> GetSongByID(int SongID);
Task<int> AddSong(Song songModel);
Task<int> UpdateSong(Song songModel);
Task<int> DeleteSong(Song songModel);
}
Now we create the class/service SongService
using HybridSongSQLite.Models;
using SQLite;
add : ISongService to implement interface (use fix)
... then add required code
private SQLiteAsyncConnection _dbConnection;
public SongService()
{
SetUpDb();
}
private async void SetUpDb()
{
if (_dbConnection == null)
{
//C:\\Users\\chiar\\AppData\\Local\\Student.db3
string dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Student.db3");
_dbConnection = new SQLiteAsyncConnection(dbPath);
await _dbConnection.CreateTableAsync<Song>();
}
}
public async Task<int> AddSong(Song songModel)
{
return await _dbConnection.InsertAsync(songModel);
}
public async Task<int> DeleteSong(Song songModel)
{
return await _dbConnection.DeleteAsync(songModel);
}
public async Task<List<Song>> GetAllSongs()
{
return await _dbConnection.Table<Song>().ToListAsync();
}
public async Task<Song> GetSongByID(int SongId)
{
var song = await _dbConnection.QueryAsync<Song>($"Select * From {nameof(Song)} where SongId={SongId} ");
return song.FirstOrDefault();
}
public async Task<int> UpdateSong(Song songModel)
{
return await _dbConnection.UpdateAsync(songModel);
}
and finally we must declare the Service in MauiProgram.cs
using HybridSongSQLite.Services;
builder.Services.AddSingleton<ISongService, SongService>();
builder.Services.AddSingleton<WeatherForecastService>();
Now it's time to focus on the UI
First we will create the SongsPage and the associated connection from the NavMenu
Declare all the usings and injections
@using HybridSongSQLite.Models
@using HybridSongSQLite.Services
@inject ISongService SongService
@inject NavigationManager Nav
Then the code section
@code {
private List<Song> songs;
protected override async Task OnInitializedAsync()
{
songs = await SongService.GetAllSongs();
}
private async void DeleteSong(Song song)
{
var response = await SongService.DeleteSong(song);
if (response > 0)
{
await OnInitializedAsync();
StateHasChanged();
}
}
private void EditSong(int Id)
{
Nav.NavigateTo("addupdatestudent/" + Id);
}
}
... and finally enter the HTML (see SongPageHTMLsnippet)
<div>
<a class="btn btn-success" href="addupdatesong"><i class="oi oi-plus"></i>Add New Song</a>
</div>
@if (songs == null)
{
<p><em>Loading...</em></p>
}
else
{
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>Title</th>
<th>Artist</th>
<th>Year</th>
<th>Gender</th>
<th>Action</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@foreach (var song in songs)
{
<tr>
<td>@song.Title</td>
<td>@song.Artist</td>
<td>@song.Year</td>
<td>@song.Gender</td>
<td>
<button type="submit" @onclick="@( () => EditSong(song.SongId))" class="btn btn-primary">Edit</button>
</td>
<td>
<button type="submit" @onclick="@( () => DeleteSong(song))" class="btn btn-primary">Delete</button>
</td>
</tr>
}
</tbody>
</table>
</div>
}
Now we create the AddUpdateSong page (used to create and edit songs)
First we add our page directives (note the use of route parameters) and our usings
@page "/addupdatesong"
@page "/addupdatesong/{SongId:int}"
@using HybridSQLiteSong.Models
@using HybridSQLiteSong.Services
@inject ISongService SongService
@inject NavigationManager Nav
Next we add our Code Section (see AddUpdateSongCodesnippet)
[Parameter]
public int SongId { get; set; }
private string title;
private string artist;
private string year;
private string gender;
private void setGender(string gender)
{
this.gender = gender;
}
protected override async Task OnInitializedAsync()
{
if (SongId > 0)
{
var response = await SongService.GetSongByID(SongId);
if (response != null)
{
title = response.Title;
artist = response.Artist;
year = response.Year;
gender = response.Gender;
}
}
}
private async void AddSongRecord()
{
var response = 0;
var songModel = new Song
{
Title = title,
Artist = artist,
Year = year,
Gender = gender,
SongId = SongId
};
if (SongId > 0)
{
//update record
if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(artist))
{
response = -1;
}
else
{
response = await SongService.UpdateSong(songModel);
}
}
else
{
if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(artist))
{
response = -1;
}
else
{
response = await SongService.AddSong(songModel);
}
}
if (response > 0)
{
await App.Current.MainPage.DisplayAlert("Record Saved", "Song " + title + " " + artist + " Added to DB", "OK");
title = "";
artist = "";
year = "";
gender = "";
StateHasChanged();
Nav.NavigateTo("songspage");
}
else
{
await App.Current.MainPage.DisplayAlert("Error", "No Record Added", "OK");
Nav.NavigateTo("songspage");
}
}
}
and finally of HTML (see AddUpdateSongHTMLsnippet)
Note the Gender HTML section
<div class="mt-2 form-group">
<label>Gender</label>
<div class=" d-flex flex-row">
<div class="col-6 d-flex justify-content-between">
<div class="form-check">
<input checked="@(gender=="Male")" @onchange="@(()=> setGender("Male"))" class="form-check-input" type="radio" name="flexRadioDefault">
<label class="form-check-label" for="flexRadioDefault1">
Male
</label>
</div>
<div class="form-check">
<input checked="@(gender=="Female")" @onchange="@(()=> setGender("Female"))" class="form-check-input" type="radio" name="flexRadioDefault">
<label class="form-check-label" for="flexRadioDefault2">
Female
</label>
</div>
</div>
</div>
</div>
Supplementary Demo
HySqLiteN9profile
.NET MAUI Blazor Hybrid .NET 9 Application with full CRUD ability using an SQLite DB
Displays a table of user profiles (Id, Name, Email) and also a QuickGrid display
Supports creating, loading, editing and deleting profiles (with confirmation dialogs).
Responsive UI: Action buttons in Table Display and QuickGrid (Edit/Delete) are hidden on Android, instead we us row taps for options which pops up a MAUI DisplayActionSheet
Navigation to add/edit profile page (UpdatePage.razor)
UpdatePage uses route parameters ie page "/addOrEdit/{id:int}"
In this Lecture we will
Demo a modified version of the FetchData/Weather page which is part of the Visual Studio Blazor Hybrid template.
We will leverage the existing WeatherForecastService and modify it to implement storing daily weather in an SQLite database. (BlazorHybridWeatherSQLiteNET8)
Key points
Configuring the SQLite Database
SQLite-net is shipped as a NuGet package. You must add the sqlite-net-pcl package to your apps to use it. Use the NuGet package manager in Visual Studio. Additionally, if you want to run an app on Android, you may also need to add the SQLitePCLRaw.provider.dynamic_cdecl package
Since our WeatherForecastService already exists (even though we will modifiy it) we can declare our service in MauiProgram.cs right now ... notice the path is set here compared to our last lecture where is was declared in the actual SongService.
//Replaced for SQLite Weather Forecast Application
//builder.Services.AddSingleton<WeatherForecastService>();
// Set path to the SQLite database (it will be created if it does not exist)
var dbPath =
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
@"WeatherForecasts.db");
// Register WeatherForecastService and the SQLite database
builder.Services.AddSingleton<WeatherForecastService>(
s => ActivatorUtilities.CreateInstance<WeatherForecastService>(s, dbPath));
Now we modify the WeatherForecast class and WeatherForecastService
public class WeatherForecast
{
//public DateTime Date { get; set; }
//public int TemperatureC { get; set; }
//public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
//public string Summary { get; set; }
[PrimaryKey, AutoIncrement]
public int Id { get; set; } //need for database implementation
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public double TemperatureF => 32 + 9.0 / 5 * TemperatureC;
[MaxLength(4000)]
public string Summary { get; set; }
}
The original WeatherForecastService hard coded (randomly) temperatures and summaries ... we will add them ourselves and store them in our SQLite Database
using SQLite;
we replace all the code and add all our methods to handle the CRUD operations
string _dbPath;
public string StatusMessage { get; set; }
private SQLiteAsyncConnection conn;
public WeatherForecastService(string dbPath)
{
_dbPath = dbPath;
}
private async Task InitAsync()
{
// Don't Create database if it exists
if (conn != null)
return;
// Create database and WeatherForecast Table
conn = new SQLiteAsyncConnection(_dbPath);
await conn.CreateTableAsync<WeatherForecast>();
}
//Here we are exposing methods that will allow us to
//Create
//Read
//Update
//Delete
public async Task<List<WeatherForecast>> GetForecastAsync()
{
await InitAsync();
return await conn.Table<WeatherForecast>().ToListAsync();
}
public async Task<WeatherForecast> CreateForecastAsync(
WeatherForecast paramWeatherForecast)
{
// Insert
await conn.InsertAsync(paramWeatherForecast);
// return the object with the
// auto incremented Id populated
return paramWeatherForecast;
}
public async Task<WeatherForecast> UpdateForecastAsync(
WeatherForecast paramWeatherForecast)
{
// Update
await conn.UpdateAsync(paramWeatherForecast);
// Return the updated object
return paramWeatherForecast;
}
public async Task<WeatherForecast> DeleteForecastAsync(
WeatherForecast paramWeatherForecast)
{
// Delete
await conn.DeleteAsync(paramWeatherForecast);
return paramWeatherForecast;
}
Finally we turn to the User Interface which for the most part is already done for us in the FetchData.razor/Weather.razor page. Here are a couple of things to note:
Edit button beside each Weather Forecast
<td>
<button class="btn btn-primary"
@onclick="@( () => EditForecast(forecast))">
Edit
</button>
</td>
Add New Forecast button below the Forecast Table
<button class="btn btn-success"
@onclick="AddNewForecast">
Add New Forecast
</button>
In the Code Section
We declare a List<WeatherForecast> forecasts instead of and array and a WeatherForecast objWeatherForecast for adding and editing
List<WeatherForecast> forecasts = new List<WeatherForecast>();
WeatherForecast objWeatherForecast = new WeatherForecast();
ShowPopup is a boolean which will be used to make a Pop-up appear and disappear (for adding and editing weather info) implementing the Bootstrap Modal dialog class
bool ShowPopup = false;
We now have 4 methods to take care of our CRUD operations
private void AddNewForecast()
{
// Make new forecast
objWeatherForecast = new WeatherForecast();
// Set Id to 0 so we know it is a new record
objWeatherForecast.Id = 0;
// Open the Popup
ShowPopup = true;
}
private void EditForecast(WeatherForecast weatherForecast)
{
// Set the selected forecast
// as the current forecast
objWeatherForecast = weatherForecast;
// Open the Popup
ShowPopup = true;
}
private async Task DeleteForecast()
{
// Close the Popup
ShowPopup = false;
try
{
Error = "";
// Delete the forecast
await ForecastService.DeleteForecastAsync(objWeatherForecast);
// Remove the Forcast
forecasts.Remove(objWeatherForecast);
}
catch (Exception ex)
{
Error = ex.Message;
}
}
private async Task SaveForecast()
{
// Close the Popup
ShowPopup = false;
Error = "";
try
{
// A new forecast will have the Id set to 0
if (objWeatherForecast.Id == 0)
{
// Create new forecast
WeatherForecast objNewWeatherForecast = new WeatherForecast();
objNewWeatherForecast.Date = System.DateTime.Now;
objNewWeatherForecast.Summary = objWeatherForecast.Summary;
objNewWeatherForecast.TemperatureC =
Convert.ToInt32(objWeatherForecast.TemperatureC);
//objNewWeatherForecast.TemperatureF = (objWeatherForecast.TemperatureC)* 9.0/5 +32;
if(!string.IsNullOrWhiteSpace(objNewWeatherForecast.Summary))
{
// Save the result
var NewWeatherForecast =
await ForecastService.CreateForecastAsync(objNewWeatherForecast);
// Add the Forcast
forecasts.Add(NewWeatherForecast);
}
}
else
{
// This is an update
//objWeatherForecast.TemperatureF = (objWeatherForecast.TemperatureC) * 9.0 / 5 + 32;
if (!string.IsNullOrWhiteSpace(objWeatherForecast.Summary))
{
await ForecastService.UpdateForecastAsync(objWeatherForecast);
}
}
await OnInitializedAsync();
StateHasChanged();
}
catch (Exception ex)
{
Error = ex.Message;
}
}
The cool feature in all of this app involves implementing the Pop-Up dialog via the modal class
After pressing the Edit or Add buttons we show the Dialog with a cancel button and either empty input boxes ready for a new weather forecast or the current forecast ready to be edited ... with the extra feature of a delete button
@if (ShowPopup)
{
<!-- This is the popup to create or edit a forecast -->
<div class="modal" tabindex="-1" style="display:block" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Edit Forecast</h3>
<!-- Button to close the popup -->
<button type="button" class="close"
@onclick="ClosePopup">
<span aria-hidden="true">X</span>
</button>
</div>
<!-- Edit form for the current forecast -->
<div class="modal-body">
<label>Celsius</label>
<input class="form-control" type="number"
placeholder="Celsius forecast"
@bind="objWeatherForecast.TemperatureC" />
@*<label>Fahrenheit</label>
<input class="form-control" type="number"
placeholder="Fahrenheit forecast"
@bind="objWeatherForecast.TemperatureF"/>*@
<input class="form-control" type="text"
placeholder="Summary"
@bind="objWeatherForecast.Summary" />
<br />
<!-- Button to save the forecast -->
<button class="btn btn-success"
@onclick="SaveForecast">
Save
</button>
<!-- Only show delete button if not a new record -->
@if (objWeatherForecast.Id > 0)
{
<!-- Button to delete the forecast -->
<button class="btn btn-danger"
@onclick="DeleteForecast">
Delete
</button>
}
</div>
</div>
</div>
</div>
}
In this Lecture we will
Review and Extend the Cross Platform abilities of Blazor ... Blazor Everywhere!
Add a Blazor component inside a Windows Form Application (WinformBlazorApp1)
We start off by creating a new project and picking a Windows Forms App ... NOT Windows Forms App (.NET Framework) . Ours will be .NET CORE
Next we must install the NuGet package Microsoft.AspNetCore.Components.WebView.WindowsForms
Now right click the project name and Edit Project File. At the top change the SDK to :
<Project Sdk="Microsoft.NET.Sdk.Razor">
Add an _Imports.razor file
@using Microsoft.AspNetCore.Components.Web
Add a folder called wwwroot and add index.html with the required markup (see indexSnippet) ... basically identical to index.html from Blazor
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WinFormsBlazor</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link href="WinFormBlazorApp.styles.css" rel="stylesheet" />
</head>
<body>
<div id="app">Loading...</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">?</a>
</div>
<script src="_framework/blazor.webview.js"></script>
</body>
</html>
Also in the wwwroot folder add a css folder and add a style sheet named app.css (hey! does this look familiar ... Blazor? ... just start up WebAssembly/Server app and copy and paste the default site.css file) ... see appcssSnippet.txt
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
h1:focus {
outline: none;
}
a, .btn-link {
color: #0071c1;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid red;
}
.validation-message {
color: red;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
Finally we add our Blazor component ... for this example we will use a slightly modified version of Counter (CounterSnippet)
<h1>Counter</h1>
<p>Current count:
<span style="color:@backgroundColor">@currentCount</span>
</p>
<button disabled="@(currentCount >= 10)" @onclick="IncrementCount">Click me</button>
<button @onclick="Reset">Reset</button>
@code {
private int currentCount = 0;
string backgroundColor = "red";
private void IncrementCount()
{
currentCount++;
backgroundColor = (currentCount % 2 == 0) ? "red" : "green";
}
private void Reset()
{
currentCount = 0;
}
}
Now we need to add this component to our WinForm
In Solution Explorer double click on the Form1.cs to open the designer
Make sure the Toolbox is visible
Locate the BlazorWebView control ... NOT WebView2 ... its under Microsoft.AspNetCore.Components.WebView.WindowsForms
Drag or click on the control to add it to the form ... it will appear on screen as WebView2
Click on this new control and go to properties on the right and change the Dock property to Fill
In the Form1 designer right click Form1 and select View Code and add the following usings
using Microsoft.AspNetCore.Components.WebView.WindowsForms;
using Microsoft.Extensions.DependencyInjection;
Inside the constructor add the following ... note .Add<Counter>
public Form1()
{
InitializeComponent();
var services = new ServiceCollection();
services.AddWindowsFormsBlazorWebView();
blazorWebView1.HostPage = "wwwroot\\index.html";
blazorWebView1.Services = services.BuildServiceProvider();
blazorWebView1.RootComponents.Add<Counter>("#app");
}
Now we can run the app
Supplementary Demos
WinformBlazorApp1Updated
Adds 3 BlazorWebView controls to the WinForm
Embeds Calculator.razor component from BlazorCalcWebAssemblyLect97.rar and the update Counter component from the first version (twice)
public Form1()
{
InitializeComponent();
var services = new ServiceCollection();
services.AddWindowsFormsBlazorWebView();
blazorWebView1.HostPage = "wwwroot\\index.html";
blazorWebView1.Services = services.BuildServiceProvider();
blazorWebView1.RootComponents.Add<Counter>("#app");
blazorWebView2.HostPage = "wwwroot\\index.html";
blazorWebView2.Services = services.BuildServiceProvider();
blazorWebView2.RootComponents.Add<Counter>("#app");
blazorWebView3.HostPage = "wwwroot\\index.html";
blazorWebView3.Services = services.BuildServiceProvider();
blazorWebView3.RootComponents.Add<Calculator>("#app");
}
WinformBlazorApp2 (Demo only)
Reviews and Extends the concept of adding Blazor components to a traditional Windows Form Application
A Native Windows Form UI appears at the top of the app with Blazor component that initially points to the Index.razor page , but then links to the Counter.razor page
This application serves as a Review and Extension of some basic concepts of adding Blazor components to a traditional Windows WinForm Application ... including
The Basic prep ... Installing the required NuGet Package Microsoft.AspNetCore.Components.WebView.WindowsForms
Updating the Project File ... to "Microsoft.NET.Sdk.Razor"
Adding a wwwroot folder with index.html ... app.css ... and NEW ... bootstrap and open-iconic
Add the razor component _Imports.razor with the required using references
Now the Extension Concepts (New!!)
Create a Pages folder and add two razor component pages ... Index (the page your on right now!) and Counter (basically the same one from the last example)
Both pages now use a Navigation Link to the other page and incorporate Bootstrap buttons
Index.razor
@page "/"
@inject NavigationManager Nav
<button class="btn btn-primary" @onclick="OnClicked">Counter</button>
@code {
private void OnClicked()
{
Nav.NavigateTo("counter");
}
}
Next we create a common Shared Folder and create (copy from a the standard Blazor template) a MainLayout.razor page ... only change is we Remove reference to NavMenu to keep it simple
Now we create a folder called Apps where we will create BlazorApp.razor identical to standard App.razor (could have just placed this file a the root level, but folder makes it more organized)
Finally we focus on our WinForm
First we drag in our webview control but don't dock it (scale it down and place it in the bottom 2/3 of the form) ... we are going to add a Native Winform section above it that contains a Label and a Button
Add a GroupBox and give it a yellow background
Drag in a Label and modify the Text property ... Create by...
Add a Button ... Click Me!
Now we go into the code section ... the key change is we reference BlazorApp.razor instead of specifically Counter.razor
public Form1()
{
InitializeComponent();
var services = new ServiceCollection();
services.AddWindowsFormsBlazorWebView();
blazorWebView1.HostPage = "wwwroot\\index.html";
blazorWebView1.Services = services.BuildServiceProvider();
//blazorWebView1.RootComponents.Add<Counter>("#app");
blazorWebView1.RootComponents.Add<BlazorApp>("#app");
this.Controls.Add(blazorWebView1);
}
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("Welcome to our 2nd Example of Blazor Everywhere!");
}
BlazorApp.razor references MainLayout and the app id used in the index.html ... which defaults to "/" ie Index.razor page
WinformBlazorApp3 (Demo only)
This next iteration of the previous exampe demonstrates how the Native Win UI can talk (share data) with the Blazor component below. Here we track the current value of the counter
First we create a class/service called AppState which we will inject into the Counter.razor page and the WinForm
public class AppState
{
public int Counter { get; set; }
}
In the Counter.razor page
@page "/counter"
@inject NavigationManager Nav
@inject WinFormBlazorApp.Data.AppState AppState
<h1>Counter</h1>
<p>Current count:
<span style="color:@backgroundColor">@AppState.Counter</span>
</p>
<button class="btn btn-info" disabled="@(AppState.Counter >= 10)" @onclick="IncrementCount">Click me</button>
<button class="btn btn-danger" @onclick="Reset">Reset</button>
<button class="btn btn-outline-success" @onclick="Return">Return to Index</button>
@code {
private int CurrentCount;
string backgroundColor = "red";
private void IncrementCount()
{
int CurrentCount = AppState.Counter;
backgroundColor = (CurrentCount % 2 == 0) ? "red" : "green";
CurrentCount++;
AppState.Counter=CurrentCount;
}
private void Reset()
{
CurrentCount = 0;
AppState.Counter=CurrentCount;
}
private void Return()
{
Nav.NavigateTo("/");
}
}
In the Form
public partial class Form1 : Form
{
private readonly AppState _appState = new();
public Form1()
{
InitializeComponent();
var services = new ServiceCollection();
services.AddWindowsFormsBlazorWebView();
services.AddSingleton<AppState>(_appState);
blazorWebView1.HostPage = "wwwroot\\index.html";
blazorWebView1.Services = services.BuildServiceProvider();
blazorWebView1.RootComponents.Add<BlazorApp>("#app");
this.Controls.Add(blazorWebView1);
}
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("Current counter value is :" + _appState.Counter);
}
}
Advanced Cross Platform example (CrossplatformWaterDemo)
This example shows a Blazor Desktop app running in both a web browser and in a WinForm and WPF wrapper.
Code shared between the Blazor Desktop apps and Blazor Server app is in the WebviewAppshared Razor Class Library.
In this Lecture we will
Learn that Localization is the process of customizing applications to display and operate in the culture of the user. This means that our app should be able to display numbers or dates differently depending on the Culture and translate the applications UI text using resource files rather than hard-coding the different possible Culture options.
Create a simple application (BlazorWasmLocalization1) that integrates Localization into a Blazor WebAssembly application using the Microsoft.Extensions.Localization NuGet package. We will outline the steps for creating the project, installing the necessary resources, and eventually configuring the app for dynamic cultural adaption, thereby enhancing the usability for a global audience.
First ... add the nuget package Microsoft.Extensions.Localization
Second we register this nuget package ...go to Program.cs and add ... builder.Services.AddLocalization();
Third ... we need to store our localization information. We are going to use Resource files (.resx) ... in our case we add folder called Rss and then add an English (Resource.resx) ... the default , a German (Resource.de-DE.resx) and a French (Resource.fr-CA.resx)... make sure to make the access modifier is set to Public
A resource file is an XML file that can contain strings and other resources, such as image file paths. Resource files are typically used to store user interface strings that must be translated into other languages. This is because you can create a separate resource file for each language into which you want to translate a Web page.
To work with Resource files you use the Resource File Editor ( pre .NET 9) or the revamped Resource Explorer .
The Resource.resx file (default) will store the English translations and the Resource.de-DE.resx will hold the German translations and the Resource.fr-CA will hold the French Canadian translations. The resource files store the translations as key-value pairs, so let’s modify them in such a manner.
helloworld ... Hello, World! ... Hallo Welt! ... Bonjour le Monde !
welcome ... Welcome to your new Localization App ... Willkommen in hrer neuen Lokalisierung App ... Bienvenue a ton nouvea localisation appli
Open Resource file using XML(Text) Editor to show contents
See ... Letter codes of cultures (languages, countries / regions) - list ... Resource Links
fr-CA ... language-Culture
Fourth ... go to _Imports.razor and add using
@using Microsoft.Extensions.Localization
@using BlazorWasmLocalization1.Rss
Now just go to each page you want to Localize and inject IStringLocalizer<Resource> localizer ... where <Resource> must match name of Resource files
... Then at each spot you want to implement some localization you reference the instance and use [name] ... see code for this Index.razor page
We will be localizing ... Hello World and Welcome to your new app
@page "/"
@inject IStringLocalizer<Resource> localizer
<PageTitle>Index</PageTitle>
<h5>Welcome to the Blazor Localized Application </h5>
@* <h1>Hello, world!</h1> *@
<h1>@localizer["helloworld"]</h1>
@localizer["welcome"]
@* Welcome to your new app. *@
To test out this version of the app you must manually change the language of your Browser.
If you are using Chrome ...
go to settings
click languages ... add languages
search for French (Canada) and check
then click on the three dots menu to the right and move French to the top
go back to the application and refresh the page, you should see the app now in French
In this Lecture we will
Modify our previous application so that users can choose the culture from the application itself (dynamically) instead of modifying the language by hand in the browser. (BlazorWasmLocalization2)
The user changes the language
We change the culture using a component that calls an extension we provide
We redirect the user back to the original page
The application shows the correct language
The language is saved locally , so that when we start our application again, it is automatically recalled.
Start off by adding a dropdown list that will appear right beside the About link
We need to create a non-routable component that we can add to the MainLayout.razor page. We will call this component CultureSelector (razor and code behind file)and add it to the Shared folder.
<strong>Culture:</strong>
<select class="form-select" @bind="Culture" style="width:300px; margin-left:10px;">
@foreach (var culture in cultures)
{
<option value="@culture">@culture.DisplayName</option>
}
</select>
Code Behind (see CultureSelectorSnippetLect173.txt)
using System.Globalization ... needed when referencing the CultureInfo class
We Inject the NavigationManager to reload current page we are on
[Inject]
public NavigationManager NavManager { get; set; }
We Inject IJSRuntime to execute blazorCulture.set javascript code which saves our Culture name locally
[Inject]
public IJSRuntime JSRuntime { get; set; }
We create an array of type CultureInfo which will store our chosen cultures ("en-US", "de-DE" , "fr-CA" ) ... this will be used to populate our Dropdown List
CultureInfo[] cultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("de-DE"),
new CultureInfo("fr-CA")
We create a Culture property of type CultureInfo .This property returns the current culture used in the application.
Also, when we select a new culture in our drop-down list, this property does some logic in the set part.
We first check if the current culture is different than a selected one.
If it is, we use JSInterop to invoke the Javascript’s blazorCulture object with a set accessor that sets the culture name in the local storage. blazorCulture.set is javascript code placed in index.html under wwwroot
Finally, we use the NavigationManager to navigate the user to the requested URI and use the forceLoad parameter to reload the page.
We do that because once we reload the app, the logic from the Program.cs class will trigger and will set a new culture as a default one.... The Program.cs update is performed after we create our WebAssemblyHostExtension
CultureInfo Culture
{
get => CultureInfo.CurrentCulture;
set
{
if (CultureInfo.CurrentCulture != value)
{
var js = (IJSInProcessRuntime)JSRuntime;
js.InvokeVoid("blazorCulture.set", value.Name);
//blazorCulture.set is javascript code placed in index.html under wwwroot
NavManager.NavigateTo(NavManager.Uri, forceLoad: true);
}
}
}
Program.cs
Register Localization service
builder.Services.AddLocalization();
var host = builder.Build();
await host.SetDefaultCulture();
await host.RunAsync();
Let's now modify the index.html to add the Javascript object that we call to set (in CultureSelector.razor.cs) and get (WebAssemblyHostExtension.cs) the culture name in the local storage (see indexhtmlSnippetLect173.txt)
<script>
window.blazorCulture = {
get: () => localStorage['BlazorCulture'],
set: (value) => localStorage['BlazorCulture'] = value
};
</script>
Next we need to set the Default Culture for the Application. We do this by creating a WebAssemblyHostExtension extension to use the local storage culture
First let's create a new Extensions folder and inside of it a new WebAssemblyHostExtension class. We are going to set the culture when we load the app
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.JSInterop;
using System.Globalization;
//We create this extension class and method SetDefaultCulture to remove the extra logic from the Program.cs class.
//In this extension method, we extend the WebAssemblyHost type and use
//JSInterop to call the get accessor from the blazorCulture Javascript object.
//This get accessor will return the culture name from the locale storage.
//If the name is returned, we create a new CultureInfo object with that name, otherwise,
//we create a new CultureInfo object with the en-US as a parameter.
//Finally, we set the DefaultThreadCurrentCulture and the DefaultThreadCurrentUICulture
//properties to the created culture.
public static class WebAssemblyHostExtension
{
public async static Task SetDefaultCulture(this WebAssemblyHost host)
{
var jsInterop = host.Services.GetRequiredService<IJSRuntime>();
var result = await jsInterop.InvokeAsync<string>("blazorCulture.get");
CultureInfo culture;
if (result != null)
culture = new CultureInfo(result);
else
culture = new CultureInfo("en-US");
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
}
}
Now we can modify the Program.cs class
//Register Localization service
builder.Services.AddLocalization();
//note: host here matches host used in WebAssemblyHostExtensions.cs so that we can execute the SetDefaultCulture Task located there
var host = builder.Build();
await host.SetDefaultCulture();
await host.RunAsync();
//await builder.Build().RunAsync();
Now we can add our CultureSelector to the MainLayout.razor file.
<CultureSelector/>
And finally we update the PropertyGroup in the Project File (Edit Project File ... Right click Project )
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Now test out the app for the various cultures
The cool thing about this Localization implementation is that it will go to the associated Resource depending on the language the browser is currently in, or the default, and any currency or date reference will change automatically to the Culture specific type ...
Note in the Index page the dates change format ... with no additional coding.
Note in the Counter page how the currency changes format depending on the Culture chosen
... also check out the FetchData page and view the dates in Culture specific format.
Learn how to use the ResXResourceManager
This tool provides central access to all ResX-based string resources in your solution. You can quickly navigate through all resource files and view the content in a well-arranged data grid. All available languages are displayed side by side in columns, to make it easy to find untranslated strings or clean up orphaned entries. All strings can be quickly edited in place, untranslated entries will be created on the fly while typing.
We create a new key and string in our default language (currentcount ... Current count:) and have ResXResourceManager translate the string into our two target languages
BlazorWasmLocalization2updated (demo only)
Using buttons or images to change Cultures instead of the Dropdown List ... on Index.razor page
<button class="btn btn-outline-primary" @onclick='() => SetLanguage("en-US")'>English</button>
<button class="btn btn-outline-primary" @onclick='() => SetLanguage("de-DE")'>German</button>
<button class="btn btn-outline-primary" @onclick='() => SetLanguage("fr-CA")'>French Canadian</button>
<br/>
<img src="images/us.jpeg" @onclick='() => SetLanguage("en-US")' style="height:50px;cursor:pointer;margin:10px"/>
<img src="images/de.jpg" @onclick='() => SetLanguage("de-DE")' style="height:50px;cursor:pointer;margin:10px" />
<img src="images/ca.png" @onclick='() => SetLanguage("fr-Ca")' style="height:50px;cursor:pointer;margin:10px" />
@code {
private void SetLanguage(string language)
{
//Here we retrieve an instance of the selected culture and all of its associated properties
//We will end up using the Name property which will return something like en-US
var lang = CultureInfo.GetCultureInfo(language);
CultureInfo.CurrentCulture = lang;
var js = (IJSInProcessRuntime)JSRuntime;
js.InvokeVoid("blazorCulture.set", lang.Name);
NavManager.NavigateTo(NavManager.Uri, forceLoad: true);
}
}
In this Lecture we will
Demo a larger implementation of Localization in a WebAssembly Application
BlazorWasmLocalization3
Implements a large Resource.resx file in the Resources folder
Note the addition in Program.cs
//New addition for Localization
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddLocalization(options => {
options.ResourcesPath = "Resources";
});
This removes the need to reference the Resources folder in _Imports.razor
Implements an enumeration (ResourceStrings.cs) of the resource string names to avoid typos ... after typing ResourceStrings. Visual Studio will display all the possible keys automatically
public enum ResourceStrings
{
ApplicationName,
CounterButton,
CounterText,
CounterTitle,
FetchDataDate,
FetchDataSubtitle,
FetchDataSummary,
FetchDataTempC,
FetchDataTempF,
FetchDataTitle,
HomeFooter1,
HomeFooter2,
HomeFooter3,
HomeSubtitle,
HomeTitle,
Language,
MenuAbout,
NavBarApplicationName,
NavBarCounter,
NavBarFetchData,
NavBarHome,
SurveyPromptTitle,
SurveyTitle
}
In each of the Pages .... Note use of :
@inject IStringLocalizer<App> Loc
@Loc[nameof(ResourceStrings.HomeTitle)]
The nameof operator accepts the name of code elements and returns a string literal of the same element. The parameters that the nameof operator can take can be a class name and all its members like methods, variables and constants and returns the string literal.
BlazorWasmLocalization4 (Dynamic Implementation of Version 3)
Allows the user to select the language and have this language stored inside the local storage
Does not rely on any Javascript to access local storage but rather implements the nuget package Blazored.LocalStorage to store and retrieve the selected culture.
BlazoredLocalStorageWASMintro
This is a quick simple introduction to implementing the Nuget Package Blazored.LocalStorage in a Blazor WebAssembly application.
First ... add the nuget package Blazored.LocalStorage
Second ...go to Program.cs and add ... using Blazored.LocalStorage; and builder.Services.AddBlazoredLocalStorage();
Third ... go to _Imports.razor and add using Blazored.LocalStorage
Now you are ready to update the Index.razor page.
inject ILocalStorageService localStorage
<h5> Your Local Storage Note</h5>
<br/>
<textarea @bind="noteContent"/>
<br/>
<div>
<button class="btn btn-primary" @onclick="@(()=>UpdateLocalStorage(noteContent))">Save</button>
<button class="btn btn-danger" @onclick="ClearLocalStorage">Clear Local Storage</button>
<button class="btn btn-success"@onclick="LoadLocalStorage">Load</button>
<button class="btn btn-info" @onclick="Clear">Clear Text Area</button>
</div>
@noteContent
To save to local storage you use localStorage.SetItemAsync(noteKey, n);
To read from local storage you use noteContent = await localStorage.GetItemAsync(noteKey);
@code {
const string noteKey = "note";
string? noteContent;
public async void LoadLocalStorage()
{
noteContent = await localStorage.GetItemAsync<string>(noteKey);
StateHasChanged();
}
public void UpdateLocalStorage(string n)
{
localStorage.SetItemAsync(noteKey, n);
}
public void ClearLocalStorage()
{
noteContent = "";
localStorage.ClearAsync();
}
public void Clear()
{
noteContent = "";
}
}
... Now back to our main application BlazorWasmLocalization4
Next we create a component (CultureDropDown.razor) in the Shared folder for our Dropdown List.
@using System.Globalization
@inject ILocalStorageService localStorage
@inject NavigationManager NavManager
<strong>Culture:</strong>
<select class="form-select" @bind="Culture" style="width:300px; margin-left:10px;">
@foreach(var culture in cultures)
{
<option value="@culture">@culture.DisplayName</option>
}
</select>
@code {
CultureInfo[] cultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("es-MX")
};
//used to populate DropDown List
CultureInfo Culture
{
get => CultureInfo.CurrentCulture;
set
{
if (CultureInfo.CurrentCulture != value)
{
localStorage.SetItemAsync<string>("culture",value.Name);
NavManager.NavigateTo(NavManager.Uri, forceLoad: true);
}
//navigate back to same page currently on and re-load page
}
}
We will consume this component as we have done before in the MainLayout.razor page.
Now we need some kind of logic that will extract the culture chosen from local storage and assign that to the current culture ... we will do this in an Extension method we call WebAssemblyHostExtension.cs
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using System.Globalization;
namespace BlazorWasmLocalization3.Client
{
public static class WebAssemblyHostExtensions
{
public async static Task SetDefaultCulture(this WebAssemblyHost host)
{
var localStorage = host.Services.GetRequiredService<ILocalStorageService>();
var cultureStringFromLS = await localStorage.GetItemAsync<string>("culture");
//part of system commands ... not created by me
CultureInfo cultureInfo;
if (!string.IsNullOrEmpty(cultureStringFromLS))
{
cultureInfo = new CultureInfo(cultureStringFromLS);
}
else
{
cultureInfo = new CultureInfo("en-US");
}
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
}
}
}
Finally we need a place to call this WebAssemblyHostExtension ... we will do this in the Program.cs file.
//await builder.Build().RunAsync();
//replaced line above with the lines below
var host = builder.Build(); //connected to (this WebAssembly host) from Extension
await host.SetDefaultCulture();
await host.RunAsync();
Don't forget to update the Project file as we have done previously
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
View the contents of "Local Storage" via the Chrome Developer Tools (F12) ... under the Applications Menu
Will offer you a number of suggested exercises ... SuggestedExercises.rar
COVID10UpdateAdd
SongListWASMclickEditUpRowHighlight
... And a Supplementary Demo Application BlazorLocalTimeWASMstandaloneNET9
This application implements a component (NuGet Package ... Toolbelt.Blazor.LocalTimeText) that will determine what the equivalent time is, for a given local time, of say ...9:00 AM (or any other updated time) ... in another Time Zone in the world
For instance ... Pacific Standard Time. For me, (Mr. Chiarelli ... near Toronto Canada EST) the time would be 6:00 AM PST
That is ... 9:00 AM EST is equivalent to 6:00 AM PST
In this Lecture we will
Create a Blazor Server application which implements Localization (BlazorServerLocalization1)
Server version is a little bit different when we create the dynamic version
WebAssembly does everything on the client side including C# code
Blazor Server does some rendering on server side and some on the client side. You never send your C# code to the client. We will need to use cookies as our local storage.
Install Nuget Package Microsoft.Extensions.Localization
Create a Resources folder at the root level and add three Resource files ... App.en-US.resx, App.fr-CA.resx,App.de-DE.resx
helloworld ... Hello, World!
welcome ... Welcome to your new Localization App
Open the _Imports.razor file and add the using statement
@using Microsoft.Extensions.Localization
Open the Program.cs and add the following at line 6 after the var
builder.Services.AddLocalization(options => {
options.ResourcesPath = "Resources";
});
Now we are going into JUST the index.razor page where we will inject the IStringSerializer and replace of the appropriate strings
@inject IStringLocalizer<App> Loc
<h1>@Loc["helloworld"]</h1>
@Loc["welcome"]
Now let's add a UI element to allow the user to change the language dynamically. (BlazorServerLocalization2) ... demo only
The workflow will be:
The user changes the language
We change the culture using a component that calls an extension we provide
We redirect the user back to the original page
The application shows on the correct language
First we add a number of class files to the project (in Data folder)
CultureWithName.cs
public record CultureWithName
{
public string Name { get; init; } = default!;
public string Culture { get; init; } = default!;
public CultureWithName(string name, string culture)
{
Name = name;
Culture = culture;
}
}
LocalizerSettings.cs (this is a static class that returns a list of available cultures with both a name and a culture string. (Optional enrichment ... not required)
Now we update Program.cs ... just below app.UseRouting
//Updates for Localization ... down to MapController
var supportedCultures = new[] { "en-US", "fr-CA","de-DE" };
//... Other more advanced technique implements call to LocalizerSettings.cs class in Data folder
//var supportedCultures = LocalizerSettings.SupportedCultures;
var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
//The above commands will basically give the results indicated below
//var localizationOptions = new RequestLocalizationOptions()
// .SetDefaultCulture("en-US")
// .AddSupportedCultures("en-US","fr-CA","de-DE")
// .AddSupportedUICultures("en-US","fr-CA","de-DE");
app.UseRequestLocalization(localizationOptions);
Here is where we are using an implementation unique to the Server solution ... we can't use LocalStorage as we did in the WebAssembly version so we will use Cookies instead.
We will need a way to store and retrieve the selected culture. For this we will use a cookie and a controller (a little bit of MVC).
Add a Controllers folder, and to it add the following
CultureController.cs (MVC Controller Empty)
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Localization;
namespace BlazorServerLocalization1.Controllers
{
[Route("[controller]/[action]")]
public class CultureController : Controller
{
//After you choose a culture from the Drop Down List
//This will set a cookie on the users side to tell
//your app which Culture preference to use
public IActionResult Set(string culture, string redirectUri)
{
if (culture != null)
{
HttpContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(
new RequestCulture(culture, culture)));
}
//redirect to whatever page you were on
//when you set this culture ... redirectUri could be / or /counter or /fetchdata
return LocalRedirect(redirectUri);
}
//Don't forget to let Program.cs know about your use of controllers
//builder.Services.AddControllers ... after WeatherForecastService
//Also need to map controllers ... add this right before app.MapBlazorHub()
//app.MapControllers();
}
}
Next we create the CultureSelector.razor component which we will reference in MainLayout.razor
@inject NavigationManager Navigation
@inject IStringLocalizer<App> Loc
@using System.Globalization
<span>
@Loc["Language"]:
<select @bind="Culture">
@foreach (var culture in SupportedCulturesWithName)
{
<option value="@culture.Culture">@culture.Name</option>
}
</select>
</span>
@code
{
//Create a list of all the available Cultures with
//with both a name and a culture string
List<CultureWithName> SupportedCulturesWithName =
new List<CultureWithName>()
{
new CultureWithName("English", "en-US"),
new CultureWithName("French (Canada)", "fr-CA"),
new CultureWithName("German","de-DE")
};
protected override void OnInitialized()
{
Culture = CultureInfo.CurrentCulture;
}
//We need a way to store and retrieve the selected culture
//For this we will use a cookie and a controller (CultureController)
//Culture property of type CultureInfo
private CultureInfo Culture
{
get => CultureInfo.CurrentCulture;
set
{
if (CultureInfo.CurrentCulture != value)
{
//Get cookie
var uri = new Uri(Navigation.Uri)
.GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
var cultureEscaped = Uri.EscapeDataString(value.Name); //will for example hold fr-CA
var uriEscaped = Uri.EscapeDataString(uri); //uri holds '/' uriEscaped holds %2F
//%2F url decodes to / when passed to Controller
//URL encoding converts characters into a format that can be transmitted over the Internet.
//So, "/" is actually a seperator, but "%2f" becomes an ordinary character that simply represents
// "/" character in element of your url.
//using query parameters
// Navigation.NavigateTo(
// $"Culture/Set?culture={cultureEscaped}&redirectUri={uriEscaped}",
// forceLoad: true);
//Save cookie
Navigation.NavigateTo("Culture/Set?culture=" + cultureEscaped
+ "&redirectUri=" + uriEscaped, forceLoad: true);
}
}
}
}
Run the app and switch cultures to test out.
Press F12 to enter Developer Tools and look at Cookies content ... should see selected culture in value string
Supplementary Demo
BlazorLocalizationNET8
In this application we will implement Localization
Using .NET 8
... using Server Interactive render mode
... with Global Interactivity Location
Note: in App.razor
<body>
<Routes @rendermode="InteractiveServer" />
<script src="_framework/blazor.web.js"></script>
</body>
In this Lecture we will
Learn how to implement Localization in a Blazor Hybrid (.NET MAUI Blazor) application. (LocalizationBlazorHybrid1)
First we install the NuGet Package Microsoft.Extensions.Localization ... Version 6 not Version 7
Next we need to go to MauiProgram.cs and make our app aware of our Localization package
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddLocalization();
return builder.Build();
Next we will go into the Resources folder and create a sub-folder called Languages
In this folder we will create several Resource files
Start off and create a default file MyStrings.resx (this will be in English in our case) ... default resource files also have an associated C# code behind file
HelloWorld .... Hello, World ! ... Bonjour le Monde ... De teller staat op
CountTitle .... Current Count Is ... Nombre actuel ... Hallo, Wereld!
You can create any two other files of your choice ... I created MyStrings.fr-CA.resx and MyStrings.nl-NL.resx
Make sure all the keys are exactly the same and are in all the different language resource files and make sure the access modifier is set to public
Now lets go to _Imports.razor and make a couple of using declarations that will be available to all our pages
@using Microsoft.Extensions.Localization
@using LocalizationBlazorHybrid.Resources.Languages
We can now do a simple early implementation of Localization
Lets go to the Index.razor page and add ...
@page "/"
@inject IStringLocalizer<MyStrings> Localizer
<h1>@Localizer["HelloWorld"]</h1>
Test it out on the Windows Machine and Android Emulator
Now lets go to the Counter.razor page and localize the contents of a Dialog in the Code section as opposed to the HTML section.
private async void IncrementCount()
{
currentCount++;
var result = await Application.Current.MainPage.DisplayAlert(Localizer["CountTitle"] + " " + currentCount, " Want another number ?", "Yes", "No");
await Application.Current.MainPage.DisplayAlert("Alert", "You replied " + (result ? "Yes" : "No"), "OK");
if (result)
{
IncrementCount();
}
}
Modify our previous application so that users can choose the culture from the application itself (dynamically) instead of modifying the language by hand in the browser. (LocalizationBlazorHybrid2)
First add a new key string element to all the Resource files
Language .... Culture: ... Culture: ... Cultuur:
Let's go into the Pages folder and update the Index.razor routable component
Our HTML section will use basically the same code we have used to display a list of possible Cultures in a DropDown List (see code snippet ... CultureSelectorHTML.txt)
<strong>@Localizer["Language"]</strong>
<select class="form-select" @bind="Culture" style="width:300px; margin-left:10px;">
@foreach (var culture in cultures)
{
<option value="@culture">@culture.DisplayName</option>
}
</select>
Our Code section is slightly modified
@code {
//The CultureInfo class provides culture-specific information
//such as the name for the culture,calendar used and formatting for dates
//and numbers ... Note above culture.DisplayName
//This creates and initializes the CultureInfo for these specific languages
//Which gives us access to a number of different properties
CultureInfo[] cultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("nl-NL"),
new CultureInfo("fr-CA")
};
//Culture property used in the bind above for the
//Dropdown List (select)
CultureInfo Culture
{
//go to CultureInfo a static class inside of .NET Framework
//and get the current culture
get => CultureInfo.CurrentCulture;
//if culture is different we set 4 values
//since we are dealing with various devices
set
{
if (CultureInfo.CurrentCulture != value)
{
Thread.CurrentThread.CurrentCulture = value;
Thread.CurrentThread.CurrentUICulture = value;
CultureInfo.DefaultThreadCurrentCulture = value;
CultureInfo.DefaultThreadCurrentUICulture = value;
}
}
}
}
Test out the application in both Windows Machine and Andriod
Seems to work properly except there is no persistence of the last set language ... that is ... it does not remember the language setting before we exited.
So we have a number of small additions to make (LocalizationBlazorHybrid3.rar)
To the bottom of the set command (on Index.razor page ) add
//We have access to all the commands from .NET MAUI Essentials
//So we use Preferences to save the current value into Local Storage
//lang is a key ... you can use any name you like
//value.Name will hold for example fr-CA
Preferences.Set("lang", value.Name);
Next we go to App.xaml.cs ... which is like the entry point of our application we add
//Get language ... make sure you use same key you used
//saving the language to local storage on the Index.razor page
//The second parameter is the default language
//if no language has been saved yet
//Next we create and initialize the CultureInfo to
//the saved language which just loaded in
var curlanguage = Preferences.Get("lang", "en-US");
var culture = new CultureInfo(curlanguage);
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture ;
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
MainPage = new MainPage();
Offer some suggested exercises that implement Localization.
Multilingual Job Application form
There are a number of flavors of ASP.NET, Web Forms (Web Sites and Web Applications), Model-View-Controller (MVC) , Razor Pages and the newest one Blazor. This course is aimed at anyone who wants to create dynamic websites using all these models , with ASP.NET Web Forms as the starting point and Blazor as the eventual ending point receving the most emphasis.
ASP.NET is the Microsoft platform for developing Web Applications. Using ASP.NET you can create e-commerce sites, data driven portals and just about anything else you can find on the internet. Best of all, you don't need to paste together a jumble of HTML and JavaScript code. Instead you can create full scale web apps by leveraging your knowledge of C# coding and a design tool like Visual Studio.
In recent years Microsoft has added MVC (Model View Controller) and Razor pages which offer different ways to build dynamic web pages. It's good to have a strong knowledge of all of these web application programming models before moving onto the most future forward choice Blazor.
We cover all these models at a beginners level offering a multitude of practical applications. BUT we take a really deep dive into Blazor (130 of 213 Lectures) , moving from beginners to intermediate and slightly advanced concepts including AI applications.
Our focus will be working with Visual Studio on WINDOWS machines . All coding examples are fully compatible with the LATEST Visual Studio Editions (As of 2025 Visual Studio Community 2022 and Visual Studio 2026) for WINDOWS .
Major Course Updates:
Apr 2026-
Learn how to implement the new Microsoft Agent Framework
Working with Multi-Model (ChatGPT or Ollama), Multi-Agent scenarios using Sequential ,Concurrent, Handoff and Group Chat Workflows
Working with the new OpenAI Responses API (designed for building advanced stateful AI agents with built in tools like Web Search and File Search) instead of the Chat Completions API.
Implementing Human in the Loop AI Agents using the Microsoft Agent Framework
Adding custom memory to the Microsoft Agent Framework. It enables AI agents to recall user preferences, past interactions, and domain-specific knowledge across sessions, moving beyond stateless, one-off conversations.
Implementing the Google Gemini LLM in a .NET MAUI Blazor Hybrid Application to demonstrate Google Maps Integration
216 Lectures
62+ Hours of Videos
1100 + downloadable demos (fully compatible with latest Visual Studio IDE 2022/2026)
Sample applications specifically for the current lectures
PLUS hundreds of supplementary demos and exercise solutions
... and more added monthly
Nov 2025-
New Lectures on a variety of AI topics including :
Integrating the OpenAI API within Blazor Applications
Using Open Source Tools to Run Large Language Models in Blazor
Performing AI Image Analysis and connecting the results to a Database
Reading and Creating QR codes and Bar codes
Implementing real-time speech-to-text capabilities in a Blazor Web App and sending the transcribed text to other users via Email
Performing AI Video Analysis on various videos files sources including via Webcam
Using AI (Whisper model) to perform Audio to Text Transcription and Translation and Text to Speech (Audio) generation
Performing Web Scraping and AI Analysis and connecting the results to a Database
Implementing the .NET Smart Components - AI powered UI Controls
Implementing AI in Hybrid Applications (Windows Desktop/Android/IOS)
Generating SQL Queries via AI for use in Blazor Database Applications (Natural Language to SQL)
RAG (Retrieval Augmented Generation) and Blazor ... A First Look
Sept 2021- Dec 2024
Three new sections on Blazor ... including the latest on .NET 8 and .NET 9 (110+ new lectures and more to come!)
... Think of this as a Course Within a Course all for one price
Feb 2020 -Sept 2021
Three new sections ( 50+ new lectures ) which focus on transitioning from Web Forms to MVC and then Core Razor Pages
Here's how I will help you to succeed:
o Each lecture starts with a list of objectives/speaking notes
o Every example covered in the lecture is available for download in the resources section including the objectives/speaking notes
o Almost every lecture has a set of Practice problems with full solutions provided
o My style of writing and teaching follows the KISS principle : Keep It Super Simple. I try to stay away from fancy computer terminology and try to teach like am speaking to a brand new user with little to no previous knowledge on the subject matter and I am always available for help replying most times within a day.
And finally please do not judge a book by it's cover don't judge the course by the title or this small description section, if you want to know exactly all the topics covered please go to:
COURSE CONTENT
Sections
Lectures (press the down arrow) This will open up literally thousands of lines of very detailed lecture descriptions leaving no doubt what is and what is not covered.