Demo used in the "Unlocking the power of the Fluent UI Blazor components" session at .NET Conf 2023
This project demonstrates how to use the Fluent UI Blazor Library.
note: This code is for step-by-step learning only. It does not respect development rules and/or best practices. This project presents only a small part of the possibilities offered by Fluent UI Blazor library. Some shortcuts have been applied to improve understanding of the project/code.
To use Fluent UI in your Blazor applications, you must:
-
Create a new .NET8 Blazor Web App.
-
On the Additional information screen, set "Interactivity type" to Global and uncheck Include sample pages.
-
Add the following NuGet packages, searching "Microsoft FluentUI Components"
-
Add this namespace to the _import.razor file.
@using Microsoft.FluentUI.AspNetCore.Components
-
Add this service in Program.cs.
// Add FluentUI builder.Services.AddHttpClient(); builder.Services.AddFluentUIComponents();
-
In the MainLayout.razor file, include this code to define a header, a menu, a body and a footer.
<FluentLayout> @* Header *@ <FluentHeader> .NET Conf 2023 </FluentHeader> <FluentBodyContent> <FluentStack Orientation="Orientation.Horizontal" Style="height: 100%;"> @* Left Menu *@ <FluentNavMenu Width="250" Title="Navigation menu"> <FluentNavLink Href="/" Match="NavLinkMatch.All">Home</FluentNavLink> <FluentNavLink Href="/Counter">Counter</FluentNavLink> <FluentNavLink Href="/Weather">Weather</FluentNavLink> </FluentNavMenu> @* Content *@ <div class="main-content"> @Body </div> </FluentStack> </FluentBodyContent> @* Footer *@ <FluentFooter> Microsoft 2023 </FluentFooter> </FluentLayout>
-
Add some styles in App.css.
/* Demo Styles */ body { margin: 0px; height: 100vh; } .footer { background-color: var(--neutral-layer-4); padding: 4px 0px; text-align: center; } .fluent-nav-menu { background-color: var(--neutral-layer-1); height: 100%; } .main-content { height: 100%; width: 100%; padding: 0px 16px; }
-
The result will be
-
Use these pagesin the demo site to find Icons and Emoji (we're not using emoji in this project).
-
On the Icons page, start a search on Home, Add (AddSquare) and Weather (WeatherRainShowersDay).
-
Add these Icons into the MainLayout.razor file.
<FluentNavMenu Width="250" Title="Navigation menu"> <FluentNavLink Icon="@(new Icons.Regular.Size20.Home())" Href="/" Match="NavLinkMatch.All">Home</FluentNavLink> <FluentNavLink Icon="@(new Icons.Regular.Size20.AddSquare())" Href="/Counter">Counter</FluentNavLink> <FluentNavLink Icon="@(new Icons.Regular.Size20.WeatherRainShowersDay())" Href="/Weather">Weather</FluentNavLink> </FluentNavMenu>
-
Go to the Button and Label pages to display the available styles.
-
Add a new file Counter.razor under the Components/Pages folder.
@page "/counter" <PageTitle>Counter</PageTitle> <FluentLabel Typo="Typography.PageTitle">Counter</FluentLabel> <FluentLabel role="status">Current count: @currentCount</FluentLabel> <FluentButton Appearance="Appearance.Accent" OnClick="IncrementCount"> Click me </FluentButton> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } }
-
Open these pages to display the available styles, for ProgressBar and ProgressRing.
-
Add a new file Weather.razor under the Components/Pages folder.
@page "/weather" <PageTitle>Weather</PageTitle> <FluentLabel Typo="Typography.PageTitle">Weather</FluentLabel> <FluentLabel>This component demonstrates showing data.</FluentLabel> @if (GridItems == null) { <FluentProgressRing>Loading...</FluentProgressRing> } else { <FluentDataGrid Items="@GridItems" ResizableColumns="true"> <PropertyColumn Title="Date" Property="@(c => c.Date.ToShortDateString())" Sortable="true" /> <PropertyColumn Title="Temp. (C)" Property="@(c => c.TemperatureC)" Sortable="true" /> <PropertyColumn Title="Temp. (F)" Property="@(c => c.TemperatureF)" Sortable="true" /> <PropertyColumn Title="Summary" Property="@(c => c.Summary)" Sortable="true" /> </FluentDataGrid> }
@code { private IQueryable<WeatherForecast>? GridItems; protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; // Simulate asynchronous loading to demonstrate streaming rendering await Task.Delay(500); var Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; GridItems = Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateOnly.FromDateTime(DateTime.Now).AddDays(index), TemperatureC = index * 2 + 17, Summary = Summaries[index], }).AsQueryable(); StateHasChanged(); } private class WeatherForecast { public DateOnly Date { get; set; } public int TemperatureC { get; set; } public string? Summary { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } }
-
Add a TemplateColumn including icons.
<FluentDataGrid Items="@GridItems" ResizableColumns="true"> <PropertyColumn Title="Date" Property="@(c => c.Date.ToShortDateString())" Sortable="true" /> <PropertyColumn Title="Temp. (C)" Property="@(c => c.TemperatureC)" Sortable="true" /> <PropertyColumn Title="Temp. (F)" Property="@(c => c.TemperatureF)" Sortable="true" /> <!-- ***************************************** --> <TemplateColumn Title="Icon"> @if (context.TemperatureC > 21) { <FluentIcon Value="@(new Icons.Regular.Size24.WeatherHailNight())" Color="@Color.Neutral" /> } else { <FluentIcon Value="@(new Icons.Regular.Size24.TimeAndWeather())" Color="@Color.Neutral" /> } </TemplateColumn> <!-- ***************************************** --> <PropertyColumn Title="Summary" Property="@(c => c.Summary)" Sortable="true" /> </FluentDataGrid>
-
The result will be
-
Add a new Register.razor page with this content.
@page "/register" <PageTitle>New user</PageTitle> <FluentLabel Typo="Typography.PageTitle">Register</FluentLabel> <FluentCard Style="margin: 16px 0px; padding: 16px; width: 600px; height: 500px;"> <FluentLabel Style="margin-bottom: 24px;"> Let's get you all set up so you can verify your personal account and begin setting up your profile. </FluentLabel> <EditForm Model="@Data" OnValidSubmit="ValidHandlerAsync"> <FluentTextField Label="First name: *" Placeholder="Enter your first name" Required @bind-Value="@Data.FistName" /> <FluentTextField Label="Last name:" Placeholder="Enter your last name" @bind-Value="@Data.LastName" /> <FluentDatePicker Label="Birth date:" @bind-Value="@Data.BirthDate" /> <FluentTextField Label="EMail: *" TextFieldType="TextFieldType.Email" Required Placeholder="Enter your email" @bind-Value="@Data.Email" /> <FluentCheckbox Label="I agree to all terms, conditions, privacy policy." @bind-Value="@AcceptToCreate" /> <div style="margin: 24px 0px;" /> <FluentButton Appearance="Appearance.Accent" Loading="@Loading" Disabled="@(!AcceptToCreate)" Type="ButtonType.Submit"> Create account </FluentButton> </EditForm> </FluentCard>
@code { private RegisterUser Data = new(); private bool AcceptToCreate = false; private bool Loading = false; private async Task ValidHandlerAsync() { Loading = true; // Simulate asynchronous loading await Task.Delay(1000); Loading = false; } public class RegisterUser { public string? LastName { get; set; } public string? FistName { get; set; } public DateTime? BirthDate { get; set; } public string? Email { get; set; } } }
-
Add a new item in the Left Navigation menu.
<FluentNavLink Icon="@(new Icons.Regular.Size20.PlugConnectedSettings())" Href="/Register">Register</FluentNavLink>
-
Add this new style in App.css (and check if the main-content is correct)
.main-content { height: 100%; width: 100%; padding: 0px 16px; } fluent-text-field { min-width: 100%; margin-bottom: 16px; }
-
The result will be
-
Add this class Country.cs to have a list of all countries.
note: Copy the full code from this file.
namespace DemoFluentUI.Components.Pages; using Microsoft.AspNetCore.Components; public record Country(string Code, string Name) { public string Flag => $"https://fluentui-blazor.net/_content/FluentUI.Demo.Shared/flags/{Code}.svg"; public MarkupString HtmlFlagAndName => (MarkupString) @$"<div style=""display: flex; gap: 10px;""> <img src=""{Flag}"" width=""24"" /> <div>{Name}</div> </div>"; public static IEnumerable<Country> All { get { // List generated using https://chat.openai.com/ return new Country[] { new Country("af", "Afghanistan"), new Country("al", "Albania"), new Country("dz", "Algeria"), new Country("as", "American Samoa"), new Country("ad", "Andorra"), ... // Copy All Countries from the GitHub source code (see the note above). new Country("ye", "Yemen"), new Country("zm", "Zambia"), new Country("zw", "Zimbabwe"), }; } } }
- Update the Register.razor file to add the FluentAutocomplete component
@page "/register" ... <EditForm Model="@Data" OnValidSubmit="ValidHandlerAsync"> ... @* Country *@ <FluentAutocomplete TOption="Country" Label="Select a country" Width="250px" Placeholder="Select countries" OnOptionsSearch="@OnSearchAsync" MaximumSelectedOptions="1" OptionText="@(i => i.Name)" @bind-SelectedOptions="@Data.Languages"> <OptionTemplate Context="language"> @language.HtmlFlagAndName </OptionTemplate> <MaximumSelectedOptionsMessage> Please select only one country. </MaximumSelectedOptionsMessage> </FluentAutocomplete> </EditForm>
@code { ... // Add this new method private async Task OnSearchAsync(OptionsSearchEventArgs<Country> e) { e.Items = Country.All.Where(i => i.Name.StartsWith(e.Text, StringComparison.OrdinalIgnoreCase)) .OrderBy(i => i.Name); await Task.CompletedTask; } public class RegisterUser { public string? LastName { get; set; } public string? FistName { get; set; } public DateTime? BirthDate { get; set; } public string? Email { get; set; } // Add this new line public IEnumerable<Country> Languages { get; set; } = Array.Empty<Country>(); } }
-
Open the file Home.razor and inject these two services.
@using Microsoft.FluentUI.AspNetCore.Components.DesignTokens @inject BaseLayerLuminance BaseLayerLuminance @inject AccentBaseColor AccentBaseColor
-
In the same file, add these Settings components.
<FluentStack Orientation="Orientation.Vertical" Style="margin-top: 32px;"> <FluentLabel Typo="Typography.PageTitle">Settings</FluentLabel> <FluentSelect Label="Color" Items=@(Enum.GetValues<OfficeColor>()) Height="200px" @bind-SelectedOption="@Color" /> <FluentSwitch Label="Theme" Style="margin-top: 16px;" UncheckedMessage="Light" CheckedMessage="Dark" @bind-Value="@IsDark" /> </FluentStack>
@code { private OfficeColor _color = OfficeColor.Default; private bool _isDark = false; public OfficeColor Color { get => _color; set { _color = value; var colorHex = _color.ToAttributeValue() ?? "default"; AccentBaseColor.WithDefault(colorHex.ToSwatch()); } } public bool IsDark { get => _isDark; set { _isDark = value; var luminance = _isDark ? StandardLuminance.DarkMode : StandardLuminance.LightMode; BaseLayerLuminance.WithDefault(luminance.GetLuminanceValue()); } } }
-
In the MainLayout.razor file, update the FluentLayout styles to use the predefined colors.
<FluentLayout Style="background-color: var(--neutral-layer-1); color: var(--neutral-foreground-rest);">
-
The result will be
-
Update the Registry.razor page to include an IconStart to the Create account button.
<FluentButton Appearance="Appearance.Accent" Loading="@Loading" IconStart="@(new Icons.Regular.Size20.PersonAdd())" Disabled="@(!AcceptToCreate)" Type="ButtonType.Submit"> Create account </FluentButton>
-
At the end of the MainLayout.razor file, add this line.
<FluentDialogProvider />
-
In the Registry.razor page, inject this service and add the Dialog Message.
@inject IDialogService DialogService private async Task ValidHandlerAsync() { Loading = true; // Simulate asynchronous loading await Task.Delay(1000); // Confirmation await DialogService.ShowInfoAsync( message: "Your account has been successfully created", title: "New user"); Loading = false; }
-
Add this new AccountCreatedDialog.razor file to customize the Dialog content.
@implements IDialogContentComponent @* Header *@ <FluentDialogHeader ShowDismiss="true"> <FluentStack VerticalAlignment="VerticalAlignment.Center"> <FluentIcon Value="@(new Icons.Regular.Size24.PersonAdd())" /> <FluentLabel Typo="Typography.PaneHeader"> @Dialog.Instance.Parameters.Title </FluentLabel> </FluentStack> </FluentDialogHeader> @* Footer *@ <FluentDialogFooter> <FluentButton Appearance="Appearance.Accent" OnClick="@SaveAsync"> Close </FluentButton> </FluentDialogFooter> @* Body *@ <FluentDialogBody> <FluentLabel Typo="Typography.Subject" MarginBlock="10px;"> Thanks for being awesome! </FluentLabel> <FluentLabel> Thank you for registering your new request. If your request is urgent, please use our telephone number to speak to one of our customer service representatives. You can also reach us via <a href="https://dotnet.microsoft.com" target="_blank">our documentation page</a>. </FluentLabel> </FluentDialogBody> @code { [CascadingParameter] public FluentDialog Dialog { get; set; } = default!; private async Task SaveAsync() { await Dialog.CloseAsync(); } }
-
Replace the previous DialogService.ShowInfoAsync method by this new
ShowDialogAsync<AccountCreatedDialog>
, to call the customized dialog box.// await DialogService.ShowInfoAsync("Your account has been successfully created", // "New user"); await DialogService.ShowDialogAsync<AccountCreatedDialog>(new DialogParameters() { Title = "New user", });
-
At the end of the MainLayout.razor file, add this line.
<FluentToastContainer />
đź“ť Since version V4-Preview.3, this tag was renamed to FluentToastProvider.
-
(Optional) In the App.css file, add this style to fix.
/* Fixed in FluentUI.Blazor V4 Preview 3 */ .fluent-toast { font-family: var(--body-font); }
-
Update the Registry.razor file.
<FluentToastContainer />
-
In the Registry.razor page, inject this service and add the Toast Message.
@inject IDialogService DialogService private async Task ValidHandlerAsync() { Loading = true; // Simulate asynchronous loading await Task.Delay(1000); // Confirmation // await DialogService.ShowInfoAsync("Your account has been successfully created", // "New user"); // await DialogService.ShowDialogAsync<AccountCreatedDialog>(new DialogParameters() // { // Title = "New user", // }); ToastService.ShowSuccess("Your account has been successfully created"); Loading = false; }