MAUI Blazor Native Applications

trellispark currently provides one MAUI Blazor website project:

  • UX-MAUI-SA – Uses UX-AUTH-SA configured for Stand-Alone Authentication
  • UX-MAUI-B2B – Uses MSAL client authentication library for Azure B2B authentication
  • UX-MAUI-B2C – Uses MSAL client authentication library for Azure B2C authentication

The MAUI-B2B and MAUI-B2C projects are currently unavailable.

trellispark uses Telerik Blazor controls to implement our UX-WASM-Components project library. This means that to customize and rebuild the MAUI Blazor websites you will need to obtain a Telerik license.

Project Dependencies

The UX-MAUI-SA project will pull in the following dependent projects:

  • UX-AUTH-SA – The Blazor component library that contains the authentication components.
  • UX-WASM-Components – the Blazor component library that implements the dynamic page builder
  • UX-WASM-Services – the services that maintain application state and communicate with the backend server-side Open APIs
  • UX-MAUI-Services – the services that access the device specific functionalities and feed into the UX-WASM-Components
  • Shared – the shared types used to transfer data between client and server, common interfaces and types.

The UX-MAUI-B2B project and UX-MAUI-B2C projects pull in the following dependent projects

  • UX-WASM-Components – the Blazor component library that implements the dynamic page builder
  • UX-WASM-Services – the services that maintain application state and communicate with the backend server-side Open APIs
  • UX-MAUI-Services – the services that access the device specific functionalities and feed into the UX-WASM-Components
  • Shared – the shared types used to transfer data between client and server, common interfaces and types.

MainProgram.cs

The region “Needed by trellispark” is required by all WASM projects to initialize the services required to start and run the dynamic page builder.

string jsonData = JsonConvert.SerializeObject(new
        {
            APIURL = "https://ts-restapi-dev.greatideaz.com/",
            Environment = "DEV",
            DefaultThemeName = "Light",
            DefaultThemeTelerikURL = "_content/GreatIdeaz.trellispark.UX.WASM.Components/css/Theme/trellispark.css",
            DefaultThemeAppURL = "_content/GreatIdeaz.trellispark.UX.WASM.Components/css/app.css",
            DefaultThemeLogoURL = "_content/GreatIdeaz.trellispark.UX.WASM.Components/logo.png",
            DefaultThemeFontURL = "https://fonts.googleapis.com/css2?family=Oxygen"
        });
var config = new ConfigurationBuilder()
            .AddJsonStream(new MemoryStream(System.Text.Encoding.UTF8.GetBytes(jsonData)))
            .Build();
        builder.Configuration.AddConfiguration(config);

#if DEBUG
        builder.Services.AddBlazorWebViewDeveloperTools();
#endif

builder.Services.AddMauiBlazorWebView();

string coreAPIURL = builder.Configuration["APIURL"];
builder.Services.AddSingleton<ApplicationState, ApplicationState>();
builder.Services.AddSingleton(sp => new HttpClient { BaseAddress = new Uri(coreAPIURL) });
builder.Services.AddSingleton<EventLog, EventLog>();
builder.Services.AddSingleton<MainPage>();


builder.Services.AddSingleton<HTTPClientService<UserInformation, UserInformation>, HTTPClientService<UserInformation, UserInformation>>();
builder.Services.AddSingleton<HTTPClientService<InstanceInformation, InstanceInformation>, HTTPClientService<InstanceInformation, InstanceInformation>>();
builder.Services.AddSingleton<HTTPClientService<URLParameterInformation, URLParameterInformation>, HTTPClientService<URLParameterInformation, URLParameterInformation>>();
builder.Services.AddSingleton<HTTPClientService<DashboardInformation, PBIEmbedConfig>, HTTPClientService<DashboardInformation, PBIEmbedConfig>>();
builder.Services.AddSingleton<HTTPClientService<DashboardInformation, DashboardInformation>, HTTPClientService<DashboardInformation, DashboardInformation>>();
builder.Services.AddSingleton<HTTPClientService<DocumentInformation, DocumentInformation>, HTTPClientService<DocumentInformation, DocumentInformation>>();
builder.Services.AddSingleton<HTTPClientService<MessageInformation, MessageInformation>, HTTPClientService<MessageInformation, MessageInformation>>();
builder.Services.AddSingleton<HTTPClientService<BookmarkInformation, BookmarkInformation>, HTTPClientService<BookmarkInformation, BookmarkInformation>>();
builder.Services.AddSingleton<HTTPClientService<ExecuteSQLInformation, ExecuteSQLInformation>, HTTPClientService<ExecuteSQLInformation, ExecuteSQLInformation>>();
builder.Services.AddSingleton<HTTPClientService<ExecuteCommandInformation, ExecuteCommandInformation>, HTTPClientService<ExecuteCommandInformation, ExecuteCommandInformation>>();
builder.Services.AddSingleton<HTTPClientService<ExecuteLocalCommandInformation, ExecuteLocalCommandInformation>, HTTPClientService<ExecuteLocalCommandInformation, ExecuteLocalCommandInformation>>();


builder.Services.AddScoped<IPrintingService, PrintingService>();

builder.Services.AddSingleton<IGI_Map, GI_MAUI_Map>();
builder.Services.AddSingleton<IGI_Contact, GI_MAUI_Contact>();
builder.Services.AddSingleton<IGI_Storage, GI_MAUI_Storage>();

The HttpClient service is used to establish a general network connection to the backend Open API endpoints. The base URL for the network connection is hardcoded in the jsonData object.

The EventLog service is used to log events (mostly errors) to the EventLog endpoint where they are looged to application insights and the DAS-RSS database EventLog table.

The ApplicationState service is used to maintain the applications state and act as a signaling service for page updates as the user interacts with the application.

The HTTPClientService generic instantiations are used to maintain connection to the specific backend Open API endpoints required by the application.

For a MAUI project, the IGI-X services will use the device specific services that would be available in MAUI applications. These services are defined in the UX-MAUI-Services project.

App.xaml

<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:ProjectNamespace"
             x:Class="ProjectNamespace.App">
    <Application.Resources>
        <ResourceDictionary>

            <Color x:Key="PageBackgroundColor">#512bdf</Color>
            <Color x:Key="PrimaryTextColor">White</Color>

            <Style TargetType="Label">
                <Setter Property="TextColor" Value="{DynamicResource PrimaryTextColor}" />
                <Setter Property="FontFamily" Value="OpenSansRegular" />
            </Style>

            <Style TargetType="Button">
                <Setter Property="TextColor" Value="{DynamicResource PrimaryTextColor}" />
                <Setter Property="FontFamily" Value="OpenSansRegular" />
                <Setter Property="BackgroundColor" Value="#2b0b98" />
                <Setter Property="Padding" Value="14,10" />
            </Style>

        </ResourceDictionary>
    </Application.Resources>
</Application>

Main.razor

<Router AppAssembly="@typeof(GreatIdeaz.trellispark.UX.AUTH.SA.Pages.Index).Assembly"
           AdditionalAssemblies="new[] {typeof(GreatIdeaz.trellispark.UX.WASM.Components.Pages.Instance).Assembly}">
       <Found Context="routeData">
           <RouteView RouteData="@routeData" DefaultLayout="@typeof(GreatIdeaz.trellispark.UX.AUTH.SA.Shared.MainLayout)" />
          </Found>
             <NotFound>
              <LayoutView Layout="@typeof(GreatIdeaz.trellispark.UX.AUTH.SA.Shared.MainLayout)">
               <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

The Router component needs to be extended to include Additional Assemblies for both the UX-AUTH-SA project and UX-WASM-Components projects.

Mainpage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:ProjectNamespace"
             x:Class="ProjectNamespace.MainPage"
             BackgroundColor="{DynamicResource PageBackgroundColor}">

    <BlazorWebView HostPage="wwwroot/index.html">
        <BlazorWebView.RootComponents>
            <RootComponent Selector="#app" ComponentType="{x:Type local:Main}" />
        </BlazorWebView.RootComponents>
    </BlazorWebView>

</ContentPage>

wwwwroot/index.html

Styling

<link rel="stylesheet" href="https://unpkg.com/@progress/kendo-theme-default@latest/dist/all.css" />
<link rel="stylesheet" id="FontThemeLink" href="https://fonts.googleapis.com/css2?family=Oxygen">
<link rel="stylesheet" id="TelerikThemeLink" href="_content/GreatIdeaz.trellispark.UX.WASM.Components/css/Theme/trellispark.css" />
<link rel="stylesheet" id="AppThemeLink" href="_content/GreatIdeaz.trellispark.UX.WASM.Components/css/app.css" />
<link rel="stylesheet" href="_content/GreatIdeaz.trellispark.UX.WASM.Components/css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="_content/GreatIdeaz.trellispark.UX.WASM.Components/css/open-iconic/font/css/open-iconic-bootstrap.min.css" />

The trellispark theming mechanism is setup to override the TelerikThemeLink, AppThemeLink, and FontThemeLink stylesheets that are built into the UX-WASM-Components project.

  • The TelerikThemeLink enables a UX Creator to create a new set of Telerik control themes using the Telerik them builder and upload them to cloud storage.
  • The AppThemeLink enables a UX Creator to provide a link to a CSS file in cloud storage to overide the application styling.
  • The FontThemeLink enables a UX Creator to provide a link to a font that will be applied to the application.

Creation of new Themes is defined by a Workspace Owner. Once a workspace theme has been defined, any workspace user can select it as their default theme for that workspace.

javascript

trellispark uses the Telerik Blazor controls so these lines are always required:

<script src="_content/Telerik.UI.for.Blazor/js/telerik-blazor.js" defer></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>

trellispark contains an integration with the Telerik Report Builder in the GI_Report field. To use this, you need to add the following:

Head:
<script src="https://reporting-dev.greatideaz.com/api/reports/resources/js/telerikReportViewer"></script>

Body:
<script src="_content/Telerik.ReportViewer.Blazor/interop.js" defer></script>

trellispark contains an integration with Microsoft PowerBI in the GI_PowerBI field. To use this, you need to add the following:

<script src="_content/GreatIdeaz.trellispark.UX.WASM.Components/powerbi.js"></script>
<script src="_content/GreatIdeaz.trellispark.UX.WASM.Components/pbi-client.js"></script>

the general javascript help functions used throughout the components are included in the following:

<script src="_content/GreatIdeaz.trellispark.UX.WASM.Components/helper.js"></script>

trellispark contains an integration with Google Maps in the GI_Address field that is used in both WASM and MAUI clients. To use this, you need to add the following and use your own Google API key value:

<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyAcccccc732QQo&libraries=places&v=weekly"></script>

Finally, trellispark uses font awesome:

<script src="https://kit.fontawesome.com/59a972d609.js" crossorigin="anonymous"></script>

AuthService

The following class is included in the UX-MAUI-B2B and UX-MAUI-B2C projects. It enables the authentication using the MSAL authentication library.

namespace ProjectNamespace
{

    public class AuthService
    {
        private readonly IPublicClientApplication authenticationClient;
        public AuthService()
        {
            authenticationClient = PublicClientApplicationBuilder.Create(Constants.ClientId)
               // .WithB2CAuthority(Constants.AuthoritySignIn) // Required for B2C. Not included in B2B
#if WINDOWS
            .WithRedirectUri("https://localhost")
#elif ANDROID
                .WithRedirectUri($"msal{Constants.ClientId}://auth")
#elif IOS
            .WithRedirectUri("msauth.AppIdentifier://auth")
            .WithIosKeychainSecurityGroup("AppIdentifier")
#endif
            .Build();
        }

        public async Task<AuthenticationResult> LoginAsync(CancellationToken cancellationToken)
        {
            AuthenticationResult result;
            try
            {
                result = await authenticationClient
                    .AcquireTokenInteractive(Constants.Scopes)
                    .WithTenantId(Constants.TenantId)
                    .WithPrompt(Prompt.ForceLogin)
#if ANDROID
                    .WithParentActivityOrWindow(Microsoft.Maui.ApplicationModel.Platform.CurrentActivity)
#endif
#if WINDOWS
        .WithUseEmbeddedWebView(true)
#endif
                    .ExecuteAsync(cancellationToken);
                return result;
            }
            catch (MsalClientException e)
            {
                string error = e.ToString();
                return null;
            }
        }
    }
}

OpeningPage.xaml

This Xaml content page displays the basic Signin/Signup content including a button to open the MSAL login page.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ProjectNamespace.OpeningPage"
             Title="OpeningPage">
    <VerticalStackLayout>
        <Image Source="https://greatideaz.com/wp-content/uploads/2021/09/Great-Ideaz-Logo_Rev1_no_tag.png"
               SemanticProperties.Description="Great Ideaz" 
               WidthRequest="300"
               HeightRequest="100"/>
        <Label 
            x:Name="LoginResultLabel"
            Text="Please sign-in or sign-up a new account"
            TextColor="#034EA1"
            VerticalOptions="Center" 
            HorizontalOptions="Center"
            VerticalTextAlignment="Center"
               HorizontalTextAlignment="Center"
            FontSize="20"
            FontFamily="Century Gothic, sans-serif"
            WidthRequest="400"
            HeightRequest="100" />
        <Label TextColor="Black"
               VerticalOptions="Center" 
            HorizontalOptions="Center"
               FontFamily="Century Gothic, sans-serif"
               VerticalTextAlignment="Center"
               HorizontalTextAlignment="Center"
               FontSize="20">
            <Label.FormattedText>
                <FormattedString>
                    <Span Text="By selecting " />
                    <Span Text="Sign-in&#10;"
                          FontAttributes="Bold" />
                    <Span Text="you agree to the greatideaz&#10; " />
                    <Span Text="Terms of Service "
                          FontAttributes="Bold"
                          TextColor="#034EA1"
                          TextDecorations="Underline">
                        <Span.GestureRecognizers>
                            <TapGestureRecognizer Command="{Binding TapCommand}"
                                          CommandParameter="https://greatideaz.com/termsofservice" />
                        </Span.GestureRecognizers>
                    </Span>
                    <Span Text="and "/>
                    <Span Text="Privacy Policy "
                          FontAttributes="Bold"
                          TextColor="#034EA1"
                          TextDecorations="Underline">
                        <Span.GestureRecognizers>
                            <TapGestureRecognizer Command="{Binding TapCommand}"
                                          CommandParameter="https://www.greatideaz.com/privacypolicy" />
                        </Span.GestureRecognizers>
                    </Span>
                </FormattedString>
            </Label.FormattedText>
        </Label>
        <Button
            Text="Sign-in or Sign-up"
            BackgroundColor="#50B748"
            TextColor="White"
            WidthRequest="250"
            HeightRequest="100"
            Clicked="Button_Clicked" 
            FontSize="24"
            Margin="16,16,16,16" />

        <Label TextColor="Black"
               VerticalOptions="Center" 
            HorizontalOptions="Center"
               FontFamily="Century Gothic, sans-serif"
               VerticalTextAlignment="Center"
               HorizontalTextAlignment="Center"
               FontSize="20">
            <Label.FormattedText>
                <FormattedString>
                    <Span Text="Contact Us "
                          FontAttributes="Bold"
                          TextColor="#034EA1"
                          TextDecorations="Underline">
                        <Span.GestureRecognizers>
                            <TapGestureRecognizer Command="{Binding TapCommand}"
                                          CommandParameter="https://greatideaz.com/contact-us" />
                        </Span.GestureRecognizers>
                    </Span>
                </FormattedString>
            </Label.FormattedText>
        </Label>
    </VerticalStackLayout>
</ContentPage>

OpeningPage.Xaml.cs

This is the code that supports the OpeningPage.Xaml and initiates the login.

namespace ProjectNamespace;

public partial class OpeningPage : ContentPage
{
    ApplicationState AppState { get; set; }

    public OpeningPage()
    {

        InitializeComponent();
    }

    protected override void OnHandlerChanged()
    {
        base.OnHandlerChanged();
        AppState = Handler.MauiContext.Services.GetService<ApplicationState>();
    }

    public ICommand TapCommand => new Command<string>(async (url) => await Launcher.OpenAsync(url));

    private async void Button_Clicked(object sender, EventArgs e)
    {
        var authService = new AuthService(); // most likely you will inject it in the constructor, but for simplicity let's initialize it here
        var result = await authService.LoginAsync(CancellationToken.None);
        var token = result?.IdToken; // you can also get AccessToken if you need it
        if (token != null)
        {
            var handler = new JwtSecurityTokenHandler();
            var data = handler.ReadJwtToken(token);
            if (data != null)
            {

                foreach (Claim claim in data.Claims)
                {
                    if (claim.Value is not null)
                    {
                        switch (claim.Type)
                        {
                            case "oid":
                                AppState.User.UserProfileGUID = Guid.Parse(claim.Value);
                                break;
                            case "preferred_username":
                                AppState.User.UserProfileName = claim.Value;
                                break;
                            case "emails":
                                AppState.User.UserProfileName = claim.Value.Replace("[\"", "").Replace("\"]", "");
                                break;
                            case "name":
                                AppState.User.FirstName = claim.Value;
                                break;
                            case "family_name":
                                AppState.User.LastName = claim.Value;
                                break;
                        }
                    }
                }
                MainPage newPage = new();
                await Navigation.PushModalAsync(newPage);
            }
        }
    }

}
Updated on June 5, 2023

Was this article helpful?

Related Articles

Need Support?
Can’t find the answer you’re looking for? Don’t worry we’re here to help!
Contact Support

Leave a Comment