Make it all dynamic in BLAZOR – Routing, Pages and Components


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 – WOHOO ๐Ÿ™‚ Read all about it – ASP.NET Core and Blazor updates in .NET Core 3.0

    Check out Daniel Roth’s great presentation’s:
    Building Full-stack C# Web Apps with Blazor in .NET Core 3.0
    The Future of Blazor on the Client

    Jeff Hollan did a great presentation combining azure functions with blazor:
    Blazor and Azure Functions for Serverless Websites

    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):

    At github: https://github.com/GoranHalvarsson/SitecoreBlazor/blob/master/SitecoreBlazorHosted.Client/wwwroot/data/routes/en.json

    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”.

    At Github: https://github.com/GoranHalvarsson/SitecoreBlazor/blob/master/Project/BlazorSite/Components/Sublayouts/Navbar.razor

    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:

    At Github: https://github.com/GoranHalvarsson/SitecoreBlazor/blob/master/Feature/navigation/NavHeader/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.
    At Github: https://github.com/GoranHalvarsson/SitecoreBlazor/blob/master/Foundation/BlazorExtensions/CustomBlazorRouter/TheRouter.cs

    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();
        }
    }
    

    At Github: https://github.com/GoranHalvarsson/SitecoreBlazor/blob/master/Project/BlazorSite/App.razor

    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;
        }
    
    }
    

    At Github: https://github.com/GoranHalvarsson/SitecoreBlazor/blob/master/Project/BlazorSite/Components/Shared/MasterBlaster.razor
    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

    Thatโ€™s all for now folks ๐Ÿ™‚


    2 thoughts on “Make it all dynamic in BLAZOR – Routing, Pages and Components

    1. Thank you, Its pain to extend the router, they could make it much easier, all I wanted is override the refresh method so there will be no forcereload on the not found page, and I almost needed to copy the whole source folder of the routing. Anyway thanks for the article

      Like

      1. Thank you, Iโ€™m glad it helped you. Yea I agree, it should be easier to extend the router. I dont get it why the router only works for existing pages/assemblies. Thats a huge flaw

        Like

    Leave a Reply

    Fill in your details below or click an icon to log in:

    WordPress.com Logo

    You are commenting using your WordPress.com account. Log Out /  Change )

    Facebook photo

    You are commenting using your Facebook account. Log Out /  Change )

    Connecting to %s

    This site uses Akismet to reduce spam. Learn how your comment data is processed.