App is the Blazor UI root component. This article looks at how it works and demonstrates how to:

The repository for this project is here, and is based on my Blazor AllInOne Template.
You can view a demo of the components running on my Blazor.Database site here https://cec-blazor-database.azurewebsites.net/ from the highlighted links.
App is normally defined in App.razor. The same component is used in both Web Assembly and Server contexts.
In the Web Assembly context the SPA startup page contains an element placeholder which is replaced when Program starts in the Web Assembly context.
.... <body> <div id="app">Loading...</div> ... </body>
The code line that defines the replacement in Program is:
// Replace the app id element with the component App
builder.RootComponents.Add<App>("#app");
In the Server context App is declared directly as a Razor component in the Razor markup. It gets pre-rendered by the server and then updated by the Blazor Server client in the browser.
... <body> <component type="typeof(Blazor.App)" render-mode="ServerPrerendered" /> ... </body>
The App code is shown below. It's a standard Razor component, inheriting from ComponentBase.
Router is the local root component and sets AppAssembly to the assembly containing Program. On initialization it trawls Assembly for all classes with a Route attribute and registers with the NavigationChanged event on the NavigationManager Service. On a navigation event it tries to match the navigation Url to a route. If it finds one, it renders the Found render fragment, otherwise it renders NotFound.
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <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>
RouteView is declared within Found. RouteData is set to the router's current routeData object and DefaultLayout set to an application Layout Type. RouteView renders an instance of RouteData.Type as a component within either the a page specific layout or the default layout, and applies any parameters in RouteData.RouteValues.
NotFound contains a LayoutView component, specifying a layout to render any child content in.
RouteViewService is the state management service for the new components. It's registered in the WASM and Server Services. The Server version can be either a Singleton or Scoped, depending on the application needs. You could have two separate services to manage application and user contexts separately.
public class RouteViewService { .... }
In the Server it's added to Startup in ConfigServices.
services.AddSingleton<RouteViewService>();
In the Web Assembly context it's added to Program.
builder.Services.AddScoped<RouteViewService>();
RouteViewManager replaces RouteView.
It's implements RouteView's functionality. It's too large to show in it's entirety so We'll look at the key functionality in sections.
When a routing event occurs, RouteViewManager.RouteData is updated and Router re-rendered. The Renderer calls SetParametersAsync on RouteViewManager, passing the updated Parameters. SetParametersAsync checks it has a valid RouteData, sets _ViewData to null and renders the component. _ViewData is set to null to ensure the component loads the route. A valid ViewData object has precedence over a valid RouteData object in the render process.
public await Task SetParametersAsync(ParameterView parameters) { // Sets the component parameters parameters.SetParameterProperties(this); // Check if we have either RouteData or ViewData if (RouteData == null) { throw new InvalidOperationException($"The {nameof(RouteView)} component requires a non-null value for the parameter {nameof(RouteData)}."); } // we've routed and need to clear the ViewData this._ViewData = null; // Render the component await this.RenderAsync(); }
Render uses InvokeAsync to ensure the render event is run on the correct thread context. _RenderEventQueued ensures there's only only one render event in the Renderer's queue.
public async Task RenderAsync() => await InvokeAsync(() => { if (!this._RenderEventQueued) { this._RenderEventQueued = true; _renderHandle.Render(_renderDelegate); } } );
For those curious, InvokeAsync looks like this.
protected Task InvokeAsync(Action workItem) => _renderHandle.Dispatcher.InvokeAsync(workItem);
RouteViewManagers content is built as a set of components, each defined within a RenderFragment.
_renderDelegate defines the local root component, cascading itself and adding the _layoutViewFragment fragment as it's ChildContent.
private RenderFragment _renderDelegate => builder => { // We're being executed so no longer queued _RenderEventQueued = false; // Adds cascadingvalue for the ViewManager builder.OpenComponent<CascadingValue<RouteViewManager>>(0); builder.AddAttribute(1, "Value", this); // Get the layout render fragment builder.AddAttribute(2, "ChildContent", this._layoutViewFragment); builder.CloseComponent(); };
_layoutViewFragment selects the layout, adds it and sets _renderComponentWithParameters as it's ChildContent.
private RenderFragment _layoutViewFragment => builder => { Type _pageLayoutType = RouteData?.PageType.GetCustomAttribute<LayoutAttribute>()?.LayoutType ?? RouteViewService.Layout ?? DefaultLayout; builder.OpenComponent<LayoutView>(0); builder.AddAttribute(1, nameof(LayoutView.Layout), _pageLayoutType); builder.AddAttribute(2, nameof(LayoutView.ChildContent), _renderComponentWithParameters); builder.CloseComponent(); };
_renderComponentWithParameters selects the view/route component to render and adds it with the supplied parameters. A valid view take precedence over a valid route.
private RenderFragment _renderComponentWithParameters => builder => { Type componentType = null; IReadOnlyDictionary<string, object> parameters = new Dictionary<string, object>(); if (_ViewData != null) { componentType = _ViewData.ViewType; parameters = _ViewData.ViewParameters; } else if (RouteData != null) { componentType = RouteData.PageType; parameters = RouteData.RouteValues; } if (componentType != null) { builder.OpenComponent(0, componentType); foreach (var kvp in parameters) { builder.AddAttribute(1, kvp.Key, kvp.Value); } builder.CloseComponent(); } else { builder.OpenElement(0, "div"); builder.AddContent(1, "No Route or View Configured to Display"); builder.CloseElement(); } };
Out-of-the-box, Blazor layouts are defined and fixed at compile time. @Layout is Razor talk that gets transposed when the Razor is pre-compiled to:
[Microsoft.AspNetCore.Components.LayoutAttribute(typeof(MainLayout))] [Microsoft.AspNetCore.Components.RouteAttribute("/")] [Microsoft.AspNetCore.Components.RouteAttribute("/index")] public partial class Index : Microsoft.AspNetCore.Components.ComponentBase ....
To change Layouts dynamically we use RouteViewService to store the layout. It can be set from any component that injects the service.
public class RouteViewService { public Type Layout { get; set; } .... }
_layoutViewFragment in RouteViewManager chooses the layout - RouteViewService.Layout is set above the default layout in precedence.
private RenderFragment _layoutViewFragment => builder => { Type _pageLayoutType = RouteData?.PageType.GetCustomAttribute<LayoutAttribute>()?.LayoutType ?? RouteViewService.Layout ?? DefaultLayout; builder.OpenComponent<LayoutView>(0); builder.AddAttribute(1, nameof(LayoutView.Layout), _pageLayoutType); builder.AddAttribute(2, nameof(LayoutView.ChildContent), _renderComponentWithParameters); builder.CloseComponent(); };
Changing in the layout is demonstrated in the demo pages.
Dynamic Routing is a little more complicated. Router is a sealed box, so it's take it or re-write it. Unless you must, don't re-write it. We're not looking to change existing routes, just add and remove new dynamic routes.
Routes are defined at compile time and are used internally within the Router Component.
RouteView Razor Pages are labelled like this:
@page "/" @page "/index"
This is Razor talk, and gets transposed into the following in the C# class when pre-compiled.
[Microsoft.AspNetCore.Components.RouteAttribute("/")] [Microsoft.AspNetCore.Components.RouteAttribute("/index")] public partial class Index : Microsoft.AspNetCore.Components.ComponentBase .....
When Router initializes it trawls any assemblies provided and builds a route dictionary of component/route pairs.
You can get a list of route attribute components like this:
static public IEnumerable<Type> GetTypeListWithCustomAttribute(Assembly assembly, Type attribute) => assembly.GetTypes().Where(item => (item.GetCustomAttributes(attribute, true).Length > 0));
On initial render the Router register a delegate with the NavigationManager.LocationChanged event. This delegate looks up routes and triggers render events on the Router. If it finds a route it renders Found which renders our new RouteViewManager. RouteViewManager builds out the Layout and adds a new instance of the component defined in RouteData.
When it doesn't find a route, what happens depends on the IsNavigationIntercepted property of the LocationChangedEventArgs provided by the event:
NavigateTo method and sets ForceLoad.NavigateTo method and sets ForceLoad.If we can avoid causing a hard navigation events in Router, we can add a component in NotFound to handle additional dynamic routing. Not too difficult, it is our code! There's an enhanced NavLink control to help control navigation - covered later. In the event of a hard navigation event, routing will still work, but the application reloads. Any rogue navigation events should be detected and fixed during testing.
CustomRouteData holds the information needed to make routing decisions. The class looks like this with inline detailed explanations.
public class CustomRouteData
{
/// The standard RouteData.
public RouteData RouteData { get; private set; }
/// The PageType to load on a match
public Type PageType { get; set; }
/// The Regex String to define the route
public string RouteMatch { get; set; }
/// Parameter values to add to the Route when created name/defaultvalue
public SortedDictionary<string, object> ComponentParameters { get; set; } = new SortedDictionary<string, object>();
/// Method to check if we have a route match
public bool IsMatch(string url)
{
// get the match
var match = Regex.Match(url, this.RouteMatch,RegexOptions.IgnoreCase);
if (match.Success)
{
// create new dictionary object to add to the RouteData
var dict = new Dictionary<string, object>();
// check we have the same or fewer groups as parameters to map the to
if (match.Groups.Count >= ComponentParameters.Count)
{
var i = 1;
// iterate through the parameters and add the next match
foreach (var pars in ComponentParameters)
{
string matchValue = string.Empty;
if (i < match.Groups.Count)
matchValue = match.Groups[i].Value;
// Use a StypeSwitch object to do the Type Matching and create the dictionary pair
var ts = new TypeSwitch()
.Case((int x) =>
{
if (int.TryParse(matchValue, out int value))
dict.Add(pars.Key, value);
else
dict.Add(pars.Key, pars.Value);
})
.Case((float x) =>
{
if (float.TryParse(matchValue, out float value))
dict.Add(pars.Key, value);
else
dict.Add(pars.Key, pars.Value);
})
.Case((decimal x) =>
{
if (decimal.TryParse(matchValue, out decimal value))
dict.Add(pars.Key, value);
else
dict.Add(pars.Key, pars.Value);
})
.Case((string x) =>
{
dict.Add(pars.Key, matchValue);
});
ts.Switch(pars.Value);
i++;
}
}
// create a new RouteData object and assign it to the RouteData property.
this.RouteData = new RouteData(this.PageType, dict);
}
return match.Success;
}
/// Method to check if we have a route match and return the RouteData
public bool IsMatch(string url, out RouteData routeData)
{
routeData = this.RouteData;
return IsMatch(url);
}
}
For those interested, TypeSwitch looks like this (thanks to cdiggins on StackOverflow for the code):
/// ================================= /// Author: stackoverflow: cdiggins /// ================================== public class TypeSwitch { public TypeSwitch Case<T>(Action<T> action) { matches.Add(typeof(T), (x) => action((T)x)); return this; } private Dictionary<Type, Action<object>> matches = new Dictionary<Type, Action<object>>(); public void Switch(object x) { matches[x.GetType()](x); } }
The updated sections in RouteViewService are shown below. Routes holds the list of custom routes - it's deliberately left open for customization.
public List<CustomRouteData> Routes { get; private set; } = new List<CustomRouteData>(); public bool GetRouteMatch(string url, out RouteData routeData) { var route = Routes?.FirstOrDefault(item => item.IsMatch(url)) ?? null; routeData = route?.RouteData ?? null; return route != null; }
RouteNotFoundManager is a simple version of RouteViewManager.
SetParametersAsync is called when the component loads. It gets the local Url, calls GetRouteMatch on RouteViewService, and renders the component. If there's no layout, it just renders the ChildContent.
public Task SetParametersAsync(ParameterView parameters) { parameters.SetParameterProperties(this); // Get the route url var url = $"/{NavManager.Uri.Replace(NavManager.BaseUri, "")}"; // check if we have a custom route and if so use it if (RouteViewService.GetRouteMatch(url, out var routedata)) _routeData = routedata; // if The layout is blank show the ChildContent without a layout if (_pageLayoutType == null) _renderHandle.Render(ChildContent); // otherwise show the route or ChildContent inside the layout else _renderHandle.Render(_ViewFragment); return Task.CompletedTask; }
_ViewFragment either renders a RouteViewManager, setting RouteData if it finds a custom route, or the contents of RouteNotFoundManager.
/// Layouted Render Fragment private RenderFragment _ViewFragment => builder => { // check if we have a RouteData object and if so load the RouteViewManager, otherwise the ChildContent if (_routeData != null) { builder.OpenComponent<RouteViewManager>(0); builder.AddAttribute(1, nameof(RouteViewManager.DefaultLayout), _pageLayoutType); builder.AddAttribute(1, nameof(RouteViewManager.RouteData), _routeData); builder.CloseComponent(); } else { builder.OpenComponent<LayoutView>(0); builder.AddAttribute(1, nameof(LayoutView.Layout), _pageLayoutType); builder.AddAttribute(2, nameof(LayoutView.ChildContent), this.ChildContent); builder.CloseComponent(); } };
Switching the RouteView without routing has several applications. These are some I've used:
The equivalent to RouteData.
public class ViewData { /// Gets the type of the View. public Type ViewType { get; set; } /// Gets the type of the page matching the route. public Type LayoutType { get; set; } /// Parameter values to add to the Route when created public Dictionary<string, object> ViewParameters { get; private set; } = new Dictionary<string, object>(); /// Constructs an instance of <see cref="ViewData"/>. public ViewData(Type viewType, Dictionary<string, object> viewValues = null) { if (viewType == null) throw new ArgumentNullException(nameof(viewType)); this.ViewType = viewType; if (viewValues != null) this.ViewParameters = viewValues; } }
All functionality is implemented in RouteViewManager.
First some properties and fields.
/// The size of the History list used for Views. [Parameter] public int ViewHistorySize { get; set; } = 10; /// Gets and sets the view data. public ViewData ViewData { get => this._ViewData; protected set { this.AddViewToHistory(this._ViewData); this._ViewData = value; } } /// Property that stores the View History. It's size is controlled by ViewHistorySize public SortedList<DateTime, ViewData> ViewHistory { get; private set; } = new SortedList<DateTime, ViewData>(); /// Gets the last view data. public ViewData LastViewData { get { var newest = ViewHistory.Max(item => item.Key); if (newest != default) return ViewHistory[newest]; else return null; } } /// Method to check if <param name="view"> is the current View public bool IsCurrentView(Type view) => this.ViewData?.ViewType == view; /// Boolean to check if we have a View set public bool HasView => this._ViewData?.ViewType != null; /// Internal ViewData used by the component private ViewData _ViewData { get; set; }
Next a set of LoadViewAsync methods to provide a variety of ways to load a new view. The main method sets the internal viewData field and calls Render to re-render the component.
// The main method public await Task LoadViewAsync(ViewData viewData = null) { if (viewData != null) this.ViewData = viewData; if (ViewData == null) { throw new InvalidOperationException($"The {nameof(RouteViewManager)} component requires a non-null value for the parameter {nameof(ViewData)}."); } await this.RenderAsync(); } public async Task LoadViewAsync(Type viewtype) => await this.LoadViewAsync(new ViewData(viewtype, new Dictionary<string, object>())); public async Task LoadViewAsync<TView>(Dictionary<string, object> data = null) => await this.LoadViewAsync(new ViewData(typeof(TView), data));
We have already seen _renderComponentWithParameters. With a valid _ViewData object, it renders the component using _ViewData.
private RenderFragment _renderComponentWithParameters => builder => { Type componentType = null; IReadOnlyDictionary<string, object> parameters = new Dictionary<string, object>(); if (_ViewData != null) { componentType = _ViewData.ViewType; parameters = _ViewData.ViewParameters; } else if (RouteData != null) { componentType = RouteData.PageType; parameters = RouteData.RouteValues; } if (componentType != null) { builder.OpenComponent(0, componentType); foreach (var kvp in parameters) { builder.AddAttribute(1, kvp.Key, kvp.Value); } builder.CloseComponent(); } else { builder.OpenElement(0, "div"); builder.AddContent(1, "No Route or View Configured to Display"); builder.CloseElement(); } };
RouteNavLink is an enhanced NavLink control. The code is a direct copy with a small amount of added code. It doesn't inherit because NavLink is a black box. It ensures navigation is through the NavigationManager rather than Html anchor links and provides direct access to RouteView loading. The code is in the Repo - it's too long to reproduce here.
The application has RouteViews/Pages to demonstrate the new components. You can review the source code in the Repo. You can also see the pages on the Demo Site.

https://cec-blazor-database.azurewebsites.net/routeviewer
This demonstrates:
RouteView without navigation. Choose a Page and click on Go To View. The page is displayed, but the Url doesn't change! Confusing, but it demos the principle.https://cec-blazor-database.azurewebsites.net/form
This demonstrates a multipart form. There are four forms:
The forms link to data in the WeathForecastService which maintains the form state. Try leaving the form part way through and then returning. State is preserved while the SPA session is maintained.
Hopefully I've demonstrated the principles you can use to build the extra functionality into the core Blazor framework. None of the components are finished articles. Use them and develop them as you wish.
If you're reading this article a long time into the future chack here for the latest version