Direkt zum Inhalt springen

CSharp and Blazor tips

Overview


MudBlazor & BlazorHybrid tips

Porting one of my tools to Blazor hybrid, I stumbled over a number of problems. Probably I'm not the only one, so here are some tips and workarounds I had to find before my app worked as expected.

NavigateBack

Navigation in Blazor is done by declaration of the "@page" attribute and calling "NavigationManager.NavigateTo()", but there is no simple way to navigate backwards to the last page of the browser history. By extending the NavigationManager we could introduce a virtual "back" route, which could then be used in links as well as in code.

info.razor
@page "/info"

<MudAppBar Fixed="true">
    <MudIconButton Href="/back" Icon="@Icons.Material.Filled.ArrowBack" Color="Color.Inherit" />
</MudAppBar>

In the example above, the button is calling the not really existing "/back" route. We can get this convenient behaviour by intercepting the routing. This just should give you an idea, it can be put into a dedicated class and ideally we would also handle the "_eventDisposable" to stop listening to location changes.

MainLayout.razor
@inherits LayoutComponentBase

@inject NavigationManager NavigationManager
@inject IJSRuntime JsRuntime

<MudLayout>
    <MudMainContent>
        @Body
    </MudMainContent>
</MudLayout>

@code {
    private IDisposable _eventDisposable;

    protected override void OnInitialized()
    {
        _eventDisposable = NavigationManager.RegisterLocationChangingHandler(LocationChangingHandler);
    }

    /// <summary>
    /// Intercept navigation events, to implement routes with special meaning.
    /// "/back": This route uses JavaScript to navigate back to the last page, adjusting the browser history.
    /// </summary>
    /// <param name="context">The event arguments.</param>
    /// <returns>A task for async calls.</returns>
    private async ValueTask LocationChangingHandler(LocationChangingContext context)
    {
        if (IsRoute(context, "back"))
        {
            context.PreventNavigation();
            await JsRuntime.InvokeVoidAsync("history.back"); // Call javascript to navigate backwards
        }
    }

    private bool IsRoute(LocationChangingContext context, string route)
    {
        string relativePath = context.TargetLocation;
        if (relativePath.StartsWith(NavigationManager.BaseUri, StringComparison.OrdinalIgnoreCase))
        {
            relativePath = relativePath.Substring(NavigationManager.BaseUri.Length);
        }
        return string.Equals(route, relativePath, StringComparison.InvariantCultureIgnoreCase);
    }
}

Prevent back button from navigating back (Blazor/Android)

On Android the back button automatically navigates backwards when a navigation is possible, and there seems to be no way to prevent it. The "MainPage.OnBackButtonPressed()" is not called in this case. What's possible though is to intercept it already in the MainActivity.cs:

MainActivity.cs
public override bool DispatchKeyEvent(KeyEvent e)
{
    // Workaround: The back button will automatically navigate back if possible, it is not
    // possible to intercept it in MainPage.OnBackButtonPressed().
    if ((e.KeyCode == Keycode.Back) && (e.Action == KeyEventActions.Down))
    {
        bool handled = TryCloseCurrentlyOpenMenusOrDialogs();
        if (handled)
            return true;
    }
    return base.DispatchKeyEvent(e);
}

The keyboard hides it all

On Android I encountered the problem, that when the soft keyboard pops up, it hides the lower half of the screen, so one cannot see what is typed in a text input at the bottom of the page. It seems that a setting is currently missing in the default template, which will be hopefully fixed in future versions.

It didn't help to add an attribute "WindowSoftInputMode" to the MainActivity, what worked instead is this:

App.xaml.cs
public App(IServiceProvider serviceProvider)
{
    InitializeComponent();
    ...

#if ANDROID
    // Workaround: Android soft keyboard hides the lower part of the content
    Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific.Application.UseWindowSoftInputModeAdjust(
        Current.On<Microsoft.Maui.Controls.PlatformConfiguration.Android>(),
        Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific.WindowSoftInputModeAdjust.Resize);
#endif
}

App cannot startup on Windows after fresh GitHub clone

My Blazor project didn's startup on Windows, after doing a fresh clone from GitHub. It stopped with the exception "System.DllNotFoundException: Unable to load DLL 'Microsoft.ui.xaml.dll' or one of its dependencies".

After doing some research I found out that it is the file Properties\launchSettings.json which is missing, it is excluded from being automatically added to the repository. Adding it manually to the repository solved the issue.


Set focus to MudTextField after page is loaded

This is actually an easy one, but nevertheless I searched for quite some time before I found the "autofocus" property. This property is common to all controls derriving from "MudBaseInput<T>", what means that most input controls support it. Set it to true for the input control which should automatically get the focus.

<MudTextField @bind-Value="ViewModel.Name" AutoFocus="true" />
<MudTextField @bind-Value="ViewModel.Street" />

Getting rid of the focused header

Sometimes the H1 header gets the focus and a border is drawn around the header element, e.g. after pressing the <ESC> key. It seems that this behaviour is on purpose for easier access with screen readers, but it doesn't look nice. There are two ways to prevent this.

One can remove the line FocusOnNavigate, so that the header isn't autofocused at all:

Main.razor
<Router AppAssembly="@typeof(Main).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>

Or since it is only a design problem we can easily hide the border in the applications css file:

app.css
h1:focus { outline: none; }

MudTextField resets ErrorText after loosing focus

When an input field is loosig the focus, the validation jumps into action and will reset the bound "ErrorText". This happens when the input field is bound only to "Error" and "ErrorText", and doesn't take advantage of the automatic "Validation", which is useful when binding to a ViewModel.

To avoid validation after loosing the focus, we could check whether a Validation is declared at all, and if not there is no need to go through the validation process.

<MudForm>
    <MudTextFieldEx @bind-Value="ViewModel.Street"
        Label="Street"
        Error="@ViewModel.HasStreetError"
        ErrorText="@ViewModel.StreetErrorText" />
</MudForm>
	
/// <summary>
/// Deactivate validation because it removes the error information.
/// See: https://github.com/MudBlazor/MudBlazor/issues/4593
/// </summary>
/// <typeparam name="T">The generic type of the text field content.</typeparam>
public class MudTextFieldEx<T> : MudTextField<T>
{
    protected override Task ValidateValue()
    {
        if (Validation == null)
            return Task.CompletedTask; // No need to validate
        return base.ValidateValue();
    }
}

CSharp tips

Stable and parallel Mergesort

When you call the sort method of a list or of an array, you will finally use the Array.Sort() function, which implements a fast Quicksort. This can be very handy, but especially when you have to sort a list of objects with multiple attributes (like database records), there are some drawbacks. Because of these drawbacks, and because I couldn't find code for a parallel and stable sorting, I implemented a Mergesort, with following advantages over the built-in sort function:

  1. The Mergesort is stable, this means that equal elements will preserve their relative position and won't get mixed up. This allows the user to first sort by one attribute, and afterwards by another attribute.
  2. The implementation is done parallel, so it can take advantage of several available processor cores.
  3. The Mergesort needs much less comparisons. While copying objects is a cheap pointer operation, the comparison of two objects can be quite expensive.

View class: StoParallelMergeSort.cs
Download sourcecode: StoParallelMergeSort.zip