Make it all dynamic in BLAZOR – Routing, Pages and Components
G?ran Halvarsson
Experienced Fullstack & Sitecore MVP | AI Enthusiast | .NET, Azure, Kubernetes, Sitecore Specialist
Dear friends, Microsoft just released .Net Core 3.0 Yes! It happened at .NET Conf 2019.
A lot of interesting stuff ?? Like the new fast JSON API – System.Text.Json(No need for Newtonsoft anymore) and of cource C# 8 with all it’s new stuff:
- Async streams – IAsyncEnumerable(we can finally use Yield)
- Switch Expressions
- Nullable reference types – Just add <Nullable>enable<Nullable> to your proj file and you are ready to fix all your bugs ??
- Default implementations of interface members
and so much more…
But for me, the most wonderful news is that BLAZOR(Server-side) is now official – WOOHOO ?? Read all about it – ASP.NET Core and Blazor updates in .NET Core 3.0
Check out Daniel Roth’s great presentation’s:
Jeff Hollan did a great presentation combining azure functions with blazor:
I have been(and still) working on making BLAZOR play well with Sitecore. The idea is to use BLAZOR and Sitecore JSS.
To all BLAZOR fans out there let me quickly explain what Sitecore is:
Sitecore is an all in one powerful content management system with a very sophisticated marketing system/engine. It combines customer data, analytics, and marketing automation capabilities to nurture customers throughout their journey with personalized content in real-time, across any channel.
Sitecore is now in version 9.2, check out all the new cool stuff. You can even get a trial license and test it out – Sitecore free Developer Trial Program
The very cool thing about Sitecore it’s all about dynamic content with data-driven page layout.
One of the keystones of Sitecore architecture is data-driven page layout, based on addressing the location of components using placeholder keys. Components define their available placeholders in their code/markup, and are placed according to their defined placeholder on the page.
One of the many features in Sitecore is the Sitecore JSS – Build Headless JavaScript applications with the power of Sitecore
*I thought I heard a horse neighing (All horses love javascript) ??
Anyways the cool thing about Sitecore JSS, it’s using the Sitecore Layout Service which is a REST api that provides data(yml/json) for the javascript apps.
*Sitecore also provides client SDKs for React, Vue, Angular and React Native.
A typical yml/json feed for a page/route(from Sitecore Layout Service) could look something like this:
{ name: 'home', displayName: 'Home', placeholders: { jss-main: [ { componentName: 'Welcome', fields: { title: { value: 'Sitecore Experience Platform + JSS', }, text: { value: '<p>...</p>', }, logoImage: { value: { src: '/assets/img/sc_logo.png', alt: 'Logo' }, }, }, params: { titleColor: "#000000", }, placeholders: { } } ] }, children: [ // additional route objects ] }
*https://jss.sitecore.com/docs/techniques/working-disconnected/manifest-api
So how can we use this in BLAZOR? Thanks to the yml/json feeds/routes we can now make something similar in BLAZOR.
Let’s have a look at how a router(json feed) will look like in our solution, notice also the structure of the routes(we also support languages here):
Tip of the day: Want to have a fancy treeview in Github? Go and get the Octotree chrome extension
Notice the Placeholders in the router(above), a placeholder has one or many components. Each component can have fields and placeholders. Here is the navbar placeholder:
"Placeholders": { "navbar": [ { "Assembly": "Project.BlazorSite", "ComponentName": "Project.BlazorSite.Components.Sublayouts.Navbar", "Name": "navbar", "Fields": {}, "Placeholders": { "navbar-header": [ { "Assembly": "Feature.Navigation", "ComponentName": "Feature.Navigation.NavHeader.BlazorNavHeader", "Name": "navbarHeader", "Fields": { "NavHeader": { "Value": "Sitecore + Blazor", "Type": "PlainTextField" } }, "Placeholders": {} } ], "navbar-menu": [ { "Assembly": "Feature.Navigation", "ComponentName": "Feature.Navigation.NavMenu.BlazorNavMenu", "Name": "navbarMenu", "Fields": {}, "Placeholders": { "navbar-activity-item": [ { "Assembly": "Feature.Navigation", "ComponentName": "Feature.Navigation.NavLanguage.BlazorNavLanguage", "Name": "navLanguage", "Fields": {}, "Placeholders": {} } ] } } ], . . .
If we look closer to the placeholder “navbar”, you will see it has the component Project.BlazorSite.Components.Sublayouts.Navbar which has the placeholders:
“navbar-header” and “navbar-menu”.
Placeholder “navbar-header” has the component Feature.Navigation.NavHeader.BlazorNavHeader and the field NavHeader containing the value Sitecore + Blazor”.
Next placeholder “navbar-menu” has the component Feature.Navigation.NavMenu.BlazorNavMenu, where the component has the placeholder “navbar-activity-item”. This placeholder holds the Feature.Navigation.NavLanguage.BlazorNavLanguage.
As I mentioned above the “navbar” placeholder has the component Project.BlazorSite.Components.Sublayouts.Navbar. So let’s take a look at the component – Navbar.razor.
Notice the “BlazorPlaceholder” components: “navbar-header” and “navbar-menu”.
Let’s continue with placeholder “navbar-header”. According to the Route(JSON file) here should component Feature.Navigation.NavHeader.BlazorNavHeader with field “NavHeader” be rendered. Here is the BlazorNavHeader.razor:
And so it continues until all the placeholders have been rendered by the BlazorPlaceholder components – It’s all dynamic ??
The architecture is Component-based Architecture, in the Sitecore world it’s called Helix.
It’s all about dependencies and controlling it! Helix defines three layers: Project, Feature and Foundation. Where each layer has a very clearly defined purpose.
- Project modules cannot reference other projects but can reference any Features and Foundations.
- Feature modules cannot reference any Project modules or other Feature modules. They can reference Foundations
- Foundations can only reference other Foundations.
Check ou this great post if you want to know more – Sitecore Helix Basics
Ok, let’s have a look at the solution:
In the Foundation layer we will have components like customrouter, placeholder, basecomponent:
In the Feature layer we will have components like menu, breadcrumb, language switcher etc:
In the Project layer we will have have the layout components, like MasterBlaster.razor(the master layout), Navbar.razor, OneColumn.razor etc. Here is also the App component, that is where we will set the dynamic routes. Let me come back to that ??
Ok, so how does it work then?
We need BLAZOR to work with dynamic routes, for that we have created a new route component – TheRouter.
It’s more or less a copy of the Microsoft.AspNetCore.Components.Routing.Router.
Except for some changes in Refresh(adding support for languages) and introduce some new parameters, like the RoutesData parameter. We need it to set dynamic routes.
(There are also some changes in the RouteTableFactory to support the dynamic routes)
The RoutesData is set in the App.razor, which is located in Project.BlazorSite:
@using Foundation.BlazorExtensions.CustomBlazorRouter @using Foundation.BlazorExtensions.Components @using Foundation.BlazorExtensions.Services @using Foundation.BlazorExtensions @inject BlazorItemsService _blazorItemsService; <ContextStateProvider> @*<Router AppAssembly=typeof(Components.Pages.RoutesClass).Assembly />*@ @*Instead of using default router from Blazor we will have a customized version, which will allow us to add routes*@ <TheRouter RouteValues="@AllRoutes"> <NotFound> <p>Sorry, there's nothing at this address.</p> </NotFound> <Found Context="routeData"> <TheRouteView RouteData="@routeData" DefaultLayout="@typeof(Project.BlazorSite.Components.Shared.MasterBlaster)"></TheRouteView> </Found> </TheRouter> </ContextStateProvider> @code { private RouterDataRoot AllRoutes = null; protected override void OnInitialized() { AllRoutes = _blazorItemsService.CreateRoutes(); } }
A typical BLAZOR application normally has one or many pages, using a master layout(You can also have nested layouts).
But in our case we want to make it all dynamic, that also includes the pages. Instead, we will use the master layout as a page. Similar to the Default.cshtml(Layout) in an Asp.Net MVC solution or to a MasterPage in an Asp.Net Webforms solution.
Behold MasterBlaster.cshtml(located in Project.BlazorSite):
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Routing @using Foundation.BlazorExtensions.Components @using Foundation.BlazorExtensions.Extensions @using Foundation.BlazorExtensions.Services @implements IDisposable @inherits LayoutComponentBase @inject Foundation.BlazorExtensions.BlazorStateMachine _blazorStateMachine @inject NavigationManager _navigationManager; @inject LayoutService _layoutService; @inject LanguageService _languageService; @inject BlazorExtensionsInteropService _blazorExtensionsInteropService; <div class="main"> <BlazorPlaceholder Name="navbar"> @Body </BlazorPlaceholder> <main role="main" class="container"> <BlazorPlaceholder Name="main"> @Body </BlazorPlaceholder> </main> <footer class="container"> <BlazorPlaceholder Name="footer"> @Body </BlazorPlaceholder> </footer> </div> @code { [Parameter] public string Language { get; set; } [CascadingParameter] public ContextStateProvider ContextStateProvider { get; set; } protected override Task OnAfterRenderAsync(bool firstRender) { return ContextStateProvider.SaveChangesAsync(); } protected override async Task OnInitializedAsync() { _navigationManager.LocationChanged += OnLocationChanged; await Reload(); } private async void OnLocationChanged(object sender, LocationChangedEventArgs args) => await Reload(); private async Task Reload() { Language = ContextStateProvider.RouteLanguage; bool hasRouteError = Language.HasRouteError(); if (hasRouteError) { Language = _languageService.GetLanguageFromUrl(Language).TwoLetterCode; } await _layoutService.LoadRoute(Language, hasRouteError); await SetPageTitle(); StateHasChanged(); } private async Task SetPageTitle() { string pageTitle = _blazorStateMachine.GetAllBlazorItemFieldsFromCurrentRoute(null).PlainText("PageTitle")?.Value?.HtmlDecode(); if (!string.IsNullOrWhiteSpace(pageTitle)) { try { await _blazorExtensionsInteropService.SetPageTitle(pageTitle); } catch (Exception ex) { // THIS IS FOR BLAZOR-SERVER-SIDE Console.WriteLine(ex.Message); } } } public void Dispose() { _navigationManager.LocationChanged -= OnLocationChanged; } }
The method _layoutService.LoadRoute will load the “json” Route. It can be a http call or to a directory, depends on what type of app(Blazor web or Blazor Electron).
That’s basically it on a higher level ??
Next part will be the different Blazor app’s we can run with this setup, by decoupling the components and layouts it will be very easy to set up and support all the Blazor types:
- Blazor Server-side
- Blazor Webassembly
- Blazor Electron
I’ve started the work for a year ago, see post – Time travel into the future – BLAZOR + SITECORE + HELIX The ongoing work happens in the Github project – SitecoreBlazor
Stay tuned for the next post, my dear fellow Blazorians!
Goodbye Javascript libraries/frameworks Hello Blazor
UPDATE! Check out my short brief of fame at the Asp.Net Community Standup
That’s all for now folks ??
2x Sitecore Technology MVP || 6x Sitecore Certified || Senior Solutions Architect || Sitecore Trainer || XMCloud Trainer || Sitecore Consultant || ContentStack Certified || MCAD
5 年Cool stuff ... This is going to remove my dependency from our Angular development team :)
?? Marketing and Brand, Senior Writer, Creative, Leadership, Social Media Manager, Editor, Content Strategy, breakfast burritos, and hot sauce
5 年Great header image? Or greatest header image?
Founder
5 年Cool stuff Goran! Your early work with Blazor and Sitecore inspired me to develop Oqtane, a new open source modular application framework for Blazor.
Freelance Software Developer at David Vesterberg AB
5 年So time to have a Blazor course then! :)