Before we talk about custom routing in Blazor, it is important to understand the fundamentals of routing in a modern web application.
Routing is essentially the "brain" of a web application. In a traditional ASP.NET MVC application, routing is the process of directing an HTTP request to a controller which is running on the server so that it can serve up content to a client browser. In contrast, in a single-page application (SPA), the router responds to navigation actions directly in the client browser without making a request to the server to fetch new content. In both cases, routers utilize route templates which are basically patterns that describe the locations to match in order to trigger a specific rendering.
Blazor is currently able to operate in two modes - server-side mode ( using SignalR ) and client-side mode ( using WebAssembly ). In both modes the default router uses the exact same approach for defining routes within a Blazor application. When creating Blazor components, developers are expected to include an @page directive in their code which instructs the router to render the component when the Url in the client browser matches the route specified.
@page "/BlazorRoute"
Defining these routes in your components does not accomplish anything unless they are also accompanied by a router to interpret them. Within a Blazor application, the default <Router> component must be included within an App.razor file which is usually located in the root project folder.
In a Blazor server-side app:
<Router AppAssembly="typeof(Startup).Assembly" />
In a Blazor client-side app:
<Router AppAssembly="typeof(Program).Assembly" />
How this works under the covers is when a .razor file with an @page directive is compiled, the generated class is provided a RouteAttribute specifying the route template. At runtime, the router looks for component classes with a RouteAttribute and renders the component with a route template that matches the requested URL. If you are interested in how Blazor routing works in greater detail you can view the source code on GitHub.
The default Blazor router works well for the majority of situations. However in the case of a framework such as Oqtane where everything must be dynamic, it means that routing must be configurable at run-time and cannot be defined directly in component code. Luckily, Blazor is extensible and allows you to replace the default router with your own custom router.
Like everything else in Blazor, a custom router is simply a standard component. It can accept parameters and can fully control its rendering process. The only requirement is that it must be aware of any changes to the Url so that it can react accordingly.
In Oqtane our custom router is named SiteRouter and it is declared in the app.razor file ( similar to the default router).
<SiteRouter OnStateChange="@ChangeState" />
The SiteRouter expects a custom Url format which is specific to Oqtane. This Url format takes into consideration multi-tenancy ( with optional subfolders ), hierarchical definition of pages, and administrative parameters for interacting with the framework. All of these Url elements are processed at run-time based on configuration which is defined in a database.
The most critical aspect of any router is to listen to the OnLocationChanged event which is raised by Blazor 's UriHelper class. In the abbreviated code below you will see that we are using an asynchronous approach for all operations, as Blazor does not allow you to block UI threads during processing. Essentially we wire up the OnLocationChanged event and anytime the Url changes we call our Refresh method which processes the Url based on our custom database configuration. To complete the process we dynamically render a component using Blazor 's primitive component methods.
RenderFragment DynamicComponent { get; set; }
private string _absoluteUri;
protected override void OnInit()
{
_absoluteUri = UriHelper.GetAbsoluteUri();
UriHelper.OnLocationChanged += OnLocationChanged;
DynamicComponent = builder =>
{
builder.OpenComponent(0, Type.GetType("Oqtane.Client.Shared.Skin, Oqtane.Client"));
builder.CloseComponent();
};
}
public void Dispose()
{
UriHelper.OnLocationChanged -= OnLocationChanged;
}
private async void OnLocationChanged(object sender, string AbsoluteUri)
{
_absoluteUri = AbsoluteUri;
await LocationChanged();
}
public async Task LocationChanged()
{
await Refresh();
}
private async Task Refresh()
{
// process _absoluteUri using database configuration
StateHasChanged();
}
The code above has been abbreviated for simplicity but if you take the opportunity to examine the actual source code on GitHub you will notice that our Refresh method is the core of the application and contains logic for loading a variety of state for our rendering pipeline. Oqtane is a framework which renders multiple layers of nested components to create a composite user interface. So in order to pass state to the various components in the hierarchy it takes advantage of a Blazor feature known as cascading parameters.
Cascading parameters are a convenient mechanism for sharing state will all child components without the need to explicitly pass values from one component to the next in sequence as parameters. We declare our PageState at the top most level in our application; in fact, it is declared at a level that is above the SiteRouter. We do this so that we can leverage state at the application level and only reload specific aspects of the state that are specific to a Url. This provides a very efficient and responsive user experience.