MAUI Environment Ribbon - UI customization (Part 4)

Author:
|
Publish date:

Chapters in this series:

  1. Intro and basic UI
  2. Shell integration
  3. NavigationPage integration
  4. UI customization
  5. Optimization and wrap up
  6. Download the code

It was mentioned in the first post of this series that we want our ribbon to be configurable. We will address this in 3 ways:

  • Setting the color
  • Setting the text value
  • Adjusting the control's position

Here are some different variants that we will be able to achieve:

Position

If we want to place the control in different corners we need to draw the Path a bit different as it touches the individual corners and contains Label that rotates differently than the Path.

We first add an enum to list our positonal options

public enum EnvironmentRibbonPosition
{
    TopLeft,
    TopRight,
    BottomLeft,
    BottomRight
}

Next we add a method that handles the positioning to the EnvironmentRibbon control

public partial class EnvironmentRibbon
{
    // ...
    private void SetPositionValues(EnvironmentRibbonPosition position)
    {
        var pathData = string.Empty;
        var horizontalOptions = LayoutOptions.Start;
        var verticalOptions = LayoutOptions.Start;
        var environmentLabelRotation = 0;
        var environmentLabelTranslationX = 0;
        var environmentLabelTranslationY = 0;

        switch (position)
        {
            case EnvironmentRibbonPosition.TopLeft:
                pathData = "M 30,0 L 50,0 L 0,50 L 0,30 Z";
                horizontalOptions = LayoutOptions.Start;
                verticalOptions = LayoutOptions.Start;
                environmentLabelRotation = -45;
                environmentLabelTranslationX = -6;
                environmentLabelTranslationY = -6;
                break;
            case EnvironmentRibbonPosition.TopRight:
                pathData = "M 0,0 L 20,0 L 50,30 L 50,50 Z";
                horizontalOptions = LayoutOptions.End;
                verticalOptions = LayoutOptions.Start;
                environmentLabelRotation = 45;
                environmentLabelTranslationX = 6;
                environmentLabelTranslationY = -6;
                break;
            case EnvironmentRibbonPosition.BottomLeft:
                pathData = "M 0,0 L 50,50 L 30,50 L 0,20 Z";
                horizontalOptions = LayoutOptions.Start;
                verticalOptions = LayoutOptions.End;
                environmentLabelRotation = 45;
                environmentLabelTranslationX = -4;
                environmentLabelTranslationY = 4;
                break;
            case EnvironmentRibbonPosition.BottomRight:
                pathData = "M 50,0 L 50,20 L 20,50 L 0,50 Z";
                horizontalOptions = LayoutOptions.End;
                verticalOptions = LayoutOptions.End;
                environmentLabelRotation = -45;
                environmentLabelTranslationX = 4;
                environmentLabelTranslationY = 4;
                break;
        }

        RibbonPath.Data = new PathGeometryConverter().ConvertFromInvariantString(pathData) as Geometry;
        HorizontalOptions = horizontalOptions;
        VerticalOptions = verticalOptions;
        EnvironmentLabel.Rotation = environmentLabelRotation;
        EnvironmentLabel.TranslationX = environmentLabelTranslationX;
        EnvironmentLabel.TranslationY = environmentLabelTranslationY;
    }
}

The lines of the Path and the position of the Label are adjusted based on the corner where the ribbon should be added and the ribbon itself is placed in the corner by using HorizontalOptions and VerticalOptions.

Colors & Text

Next we add a configuration record to contain all of our customization needs

public record EnvironmentRibbonConfiguration
{
    public EnvironmentRibbonPosition Position { get; set; }
    public string Text { get; set; } = string.Empty;
    public Color TextColor { get; set; } = Colors.White;
    public Color BackgroundColor { get; set; } = Colors.Black;
}

To use it in the EnvironmentRibbon control we can modify the constructor to require a configuration as parameter

public partial class EnvironmentRibbon
{
    // ...

    public EnvironmentRibbon(EnvironmentRibbonConfiguration configuration)
    {
        InitializeComponent();

        EnvironmentLabel.Text = configuration.Text;
        EnvironmentLabel.TextColor = configuration.TextColor;
        RibbonPath.Fill = configuration.BackgroundColor;

        SetPositionValues(configuration.Position);
    }
}

The place where we initialize instances of EnvironmentRibbon is in EnvironmentRibbonService. So we need to get a configuration there and use it in the AddEnvironmentRibbonToPage method.

public class EnvironmentRibbonService
{
    private static EnvironmentRibbonConfiguration? _configuration;
    
    public static void SetConfiguration(EnvironmentRibbonConfiguration configuration)
    {
        _configuration ??= configuration;
    }

    // ...

    public static void AddEnvironmentRibbonToPage(Page? page)
    {
        // ...

        if(page is ContentPage contentPage)
        {
            var newRootGrid = new Grid();

            newRootGrid.Children.Add(contentPage.Content);
            newRootGrid.Children.Add(new EnvironmentRibbon(_configuration));
            contentPage.Content = newRootGrid;
        }
        
        // ...
    }
}

We also need to set the configuration when we are initializing shell and different page types, here's the Shell implementation:

public static class ShellExtensions
{
    public static Shell AddEnvironmentRibbon(this Shell shell, EnvironmentRibbonConfiguration configuration)
    {
        EnvironmentRibbonService.SetConfiguration(configuration);
        shell.Navigated += EnvironmentRibbonService.Shell_Navigated;
        return shell;
    }
}

And now we can create our customized EnvironmentRibbon in App

public partial class App
{
    public App()
    {
        InitializeComponent();

        var appShell = new AppShell()
            .AddEnvironmentRibbon(new EnvironmentRibbonConfiguration
            {
                BackgroundColor = Colors.Purple,
                TextColor = Colors.White,
                Text = "Here",
                Position = EnvironmentRibbonPosition.BottomRight,
            });

        MainPage = appShell;
    }
}

Add EnvironmentRibbonType

Last but not least - we can add some presets so that it's easier to use and the developers don't need to figure out the texts and colors if they just want to use the control without worrying about this aspect. The default behavior would be that we use the background color to display the environment and the text to use the application version - to get more information out of the control.

We first add an enum

public enum EnvironmentRibbonType
{
    Dev,
    Alpha,
    Beta
}

Now we enrich the EnvironmentRibbonService by adding support for the defaults for each type

public class EnvironmentRibbonService
{
    // ...

    public static void SetConfiguration(EnvironmentRibbonType environmentRibbonType, EnvironmentRibbonPosition environmentRibbonPosition)
    {
        SetConfiguration(CreateConfiguration(environmentRibbonType, environmentRibbonPosition));
    }

    private static EnvironmentRibbonConfiguration CreateConfiguration(EnvironmentRibbonType environmentRibbonType, EnvironmentRibbonPosition environmentRibbonPosition)
    {
        var configuration = environmentRibbonType switch
        {
            EnvironmentRibbonType.Dev => new EnvironmentRibbonConfiguration
            {
                TextColor = Colors.White,
                BackgroundColor = Colors.Red
            },
            EnvironmentRibbonType.Alpha => new EnvironmentRibbonConfiguration
            {
                TextColor = Colors.White,
                BackgroundColor = Colors.DarkOrange
            },
            EnvironmentRibbonType.Beta => new EnvironmentRibbonConfiguration
            {
                TextColor = Colors.White,
                BackgroundColor = Colors.Green
            },

            _ => throw new ArgumentOutOfRangeException(nameof(environmentRibbonType))
        };

        configuration.Text = AppInfo.Current.VersionString;
        configuration.Position = environmentRibbonPosition;

        return configuration;
    }

    // ...
}

In the code we use AppInfo.Current.VersionString to get the application's version.

When we have this we again add the support to the extension methods for shell and the pages. Here's the Shell version:

public static class ShellExtensions
{
    public static Shell AddEnvironmentRibbon(this Shell shell, EnvironmentRibbonType environmentRibbonType, EnvironmentRibbonPosition environmentRibbonPosition)
    {
        EnvironmentRibbonService.SetConfiguration(environmentRibbonType, environmentRibbonPosition);
        shell.Navigated += EnvironmentRibbonService.Shell_Navigated;
        return shell;
    }

    // ...
}

And we're ready to use our ribbon with a preset configuration

public partial class App 
{
    public App()
    {
        InitializeComponent();

        var appShell = new AppShell()
            .AddEnvironmentRibbon(EnvironmentRibbonType.Beta, EnvironmentRibbonPosition.TopRight);

        MainPage = appShell;
    }
}

Previous chapter Next chapter

Roman Jašek
Roman Jašek

BIO: 

Roman is a Microsoft MVP working as software architect at RIGANTI. His main area is mobile application development using MAUI. He also works with other areas of the .NET ecosystem including web backend using ASP.NET, Azure etc. In his spare time Roman deals with organizing talks and conferences as part of the local Windows User Group and teaches application development using .NET MAUI and Blazor at universities in the Czech Republic.