Search code examples
c#blazor-server-side

How can I get into a global variable if the user is using a mobile browser using Blazor Server?


I am facing an issue with blazor server in .NET 8.

I want to get into a global variable if the user is using a mobile browser in C#.

public class AppState
{
  public bool isMobile {get;set;}
}

//In program.cs
builder.Services.AddScoped<AppState>();

And then somewhere in my JS:

window.myApp= {
isMobile: function () {
    return ($(window).width() < 768);
 }
}

//Code to run
appState.isMobile = await JSRuntime.InvokeAsync<bool>("myApp.isMobile");

My question:

Where can I load the JS to make the property isMobile available everywhere ?

So far I've tried, or suggest :

  1. In the program.cs but I got an exception
  2. In the Routes.razor file as a CascadingValue (how ?)
  3. In the Layout page in the OnAfterRenderAsync method but then child components render first so it's useless.

Solution

  • If I understand your problem correctly, you need to render to get the screen size before you can make some UI/UX decisions. Along the way you also need to handle pre-rendering.

    The way to achieve this is to defer rendering of the normal content until the first render event has occurred, and a JS context exists.

    Anyone please comment if I've missed something critical here.

    First a modified JS script to return the screen width. I've moved the decision logic into the C# code:

    wwwroot/app.js

    window.myApp = {
        getScreenWidth: function () {
            return window.innerWidth;
        }
    }
    

    Registered in App.razor

    <body>
        <Routes @rendermode="InteractiveServer" />
        <script src="_framework/blazor.web.js"></script>
        <script src="app.js"></script>
    </body>
    

    No change to the service or registation.

    Next the critical component that gets the screen width and defers primary content rendering.

    @if (_preRendering)
    {
        //Do whatever you want to inform the user that we are prerendering 
        <div class="alert alert-info m-5">Pre-Rendering</div>
    }
    
    // only render the content when we have the AppState data.
    @if (_ModeIsSet)
    {
        @ChildContent
    }
    
    @code {
        [Parameter] public RenderFragment? ChildContent { get; set; }
        [CascadingParameter] private HttpContext? HttpContext { get; set; }
        [Inject] private AppState _appState { get; set; } = default!;
        [Inject] private IJSRuntime _js { get; set; } = default!;
    
        private bool _ModeIsSet;
        private bool _preRendering => HttpContext is not null;
    
        protected async override Task OnInitializedAsync()
        {
            // Don't do anything when pre-rendering
            if (_preRendering)
                return;
    
            // Yield so the component renders and we have a JS context to query
            await Task.Delay(10);
            var width = await _js.InvokeAsync<int>("myApp.getScreenWidth");
            _appState.IsMobile = width < 768;
    
            // Set so the normal content is rendered on the SetParametersAsync completion render 
            _ModeIsSet = true;
        }
    }
    

    Finally modify Routes.razor:

    <MobileDetect>
        <Router AppAssembly="typeof(Program).Assembly">
            <Found Context="routeData">
                <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
                <FocusOnNavigate RouteData="routeData" Selector="h1" />
            </Found>
        </Router>
    </MobileDetect>
    

    My Demo Page:

    @page "/"
    @inject AppState AppState
    <PageTitle>Home</PageTitle>
    
    <h1>Hello, world!</h1>
    
    Welcome to your new app.
    
    <div class="alert alert-primary">@this.AppState.IsMobile</div>
    

    enter image description here