Skip to content

Instantly share code, notes, and snippets.

@FlavioGoncalves-Cayas
Last active October 30, 2024 10:34
Show Gist options
  • Save FlavioGoncalves-Cayas/841a13ca1f2904ef1f732c159b4c5f5d to your computer and use it in GitHub Desktop.
Save FlavioGoncalves-Cayas/841a13ca1f2904ef1f732c159b4c5f5d to your computer and use it in GitHub Desktop.
.NET MAUI OnOrientationExtension

.NET MAUI OnOrientationExtension

This is the accompanying Gist to the blog post at cayas.de.

The OnOrientation MarkupExtension helps to make XAML pages automatically adapt to orientation changes. By enabling developers to provide different values for their view's properties directly inline in XAML, similiar to other MarkupExtensions like OnPlatform and OnIdiom.

For example it can be used to rearrange a Grid's ColumnDefinitions to accommodate for orientation changes:

<Grid ColumnDefinitions="{OnOrientation Default='*', Landscape='*,*', TypeConverter={x:Type ColumnDefinitionCollectionTypeConverter}}">

(Why ColumnDefinitionCollectionTypeConverter needs to be specified here is explained below or in the blog post)

How to use

  1. Install CommunityToolkit.Maui into the project
  2. Add UseMauiCommunityToolkit() to MauiProgram.cs when configuring the MauiAppBuilder
  3. Add both classes, OnOrientationExtension.cs and OnOrientationSource.cs, to the .NET MAUI project
  4. Add the namespace alias to the XAML file
  5. Use OnOrientation either inline as MarkupExtension in attribute syntax, or via property element syntax

Make sure to always specify a value for Default.

Inline

<Label Grid.Row="{OnOrientation Default=0, Landscape=1}" />

Since the values you provide in a MarkupExtension must be of a primitive type like int or string and type conversion using TypeConverters does not kick in, sometimes you may need to use x:Static.

<Label HorizontalOptions="{OnOrientation Default={x:Static LayoutOptions.Start}, Landscape={x:Static LayoutOptions.Center}}" />

If the value is more complex and you know which TypeConverter should be used, you can specify it. OnOrientation can then perform type conversion using the provided Type of TypeConverter.

<Grid ColumnDefinitions="{OnOrientation Default='*', Landscape='*,*', TypeConverter={x:Type ColumnDefinitionCollectionTypeConverter}}">

Property Element Syntax

When a complex value is needed and no TypeConverter exists, then you can also use property element syntax to specify the different values. Although one can argue that this is way too verbose for practical use.

<Grid.ColumnDefinitions>
    <ext:OnOrientation>
        <ext:OnOrientation.Default>
            <ColumnDefinitionCollection>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="0" />
            </ColumnDefinitionCollection>
        </ext:OnOrientation.Default>
        <ext:OnOrientation.Landscape>
            <ColumnDefinitionCollection>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
            </ColumnDefinitionCollection>
        </ext:OnOrientation.Landscape>
    </ext:OnOrientation>
</Grid.ColumnDefinitions>

This syntax could also be used to switch out entire views.

<ContentView>
    <ContentView.Content>
        <ext:OnOrientation>
            <Label Text="This text is visible in Default" />
            <ext:OnOrientation.Landscape>
                <Label Text="This text is visible in Landscape" />
            </ext:OnOrientation.Landscape>
        </ext:OnOrientation>
    </ContentView.Content>
</ContentView>

Limitations

  • Only works with Android and iOS, since OnOrientationExtension uses the Orientation provided by DeviceDisplay.MainDisplayInfo.Orientation
  • Value cannot be a BindingExpression
[ContentProperty(nameof(Default))]
public class OnOrientationExtension : IMarkupExtension<BindingBase>
{
public Type TypeConverter { get; set; }
public object Default { get; set; }
public object Landscape { get; set; }
public object Portrait { get; set; }
static OnOrientationExtension()
{
DeviceDisplay.MainDisplayInfoChanged += (_, _) => WeakReferenceMessenger.Default.Send(new OrientationChangedMessage());
}
public BindingBase ProvideValue(IServiceProvider serviceProvider)
{
var typeConverter = TypeConverter != null ? (TypeConverter)Activator.CreateInstance(TypeConverter) : null;
var orientationSource = new OnOrientationSource { DefaultValue = typeConverter?.ConvertFromInvariantString((string)Default) ?? Default };
orientationSource.PortraitValue = Portrait == null ? orientationSource.DefaultValue : typeConverter?.ConvertFromInvariantString((string)Portrait) ?? Portrait;
orientationSource.LandscapeValue = Landscape == null ? orientationSource.DefaultValue : typeConverter?.ConvertFromInvariantString((string)Landscape) ?? Landscape;
return new Binding
{
Mode = BindingMode.OneWay,
Path = nameof(OnOrientationSource.Value),
Source = orientationSource
};
}
object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) => ProvideValue(serviceProvider);
public class OrientationChangedMessage
{
}
}
public class OnOrientationSource : INotifyPropertyChanged
{
private object _defaultValue;
private object _portraitValue;
private object _landscapeValue;
public object DefaultValue
{
get => _defaultValue;
set
{
_defaultValue = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
public object PortraitValue
{
get => _portraitValue;
set
{
_portraitValue = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
public object LandscapeValue
{
get => _landscapeValue;
set
{
_landscapeValue = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
public object Value => DeviceDisplay.MainDisplayInfo.Orientation switch
{
DisplayOrientation.Portrait => PortraitValue ?? DefaultValue,
DisplayOrientation.Landscape => LandscapeValue ?? DefaultValue,
_ => DefaultValue
};
public OnOrientationSource()
{
WeakReferenceMessenger.Default.Register<OnOrientationSource,OnOrientationExtension.OrientationChangedMessage>(this, OnOrientationChanged);
}
private void OnOrientationChanged(OnOrientationSource r, OnOrientationExtension.OrientationChangedMessage m)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
public event PropertyChangedEventHandler PropertyChanged;
}
@FlavioGoncalves-Cayas
Copy link
Author

Hi @grabnerM ,
to me it sounds like you are missing the namespace declaration in your XAML. In my code OnOrientation is prefixed with ext: as that is the alias I have given for the namespace that contains the OnOrientationExtension class.
It should also work with Margins if applied correctly.
Show me your specific code and I can help you with that.

@grabnerM
Copy link

grabnerM commented Oct 21, 2024

So, I thought I had figured it out, but nope..

<VerticalStackLayout Margin="{ local:OnOrientation Portrait=40, Landscape=20, Default= 0, TypeConverter={x:Type Thickness} }">
some labels
</VerticalStackLayout>

In my opinion this should work, but the Margin does not get applied

@FlavioGoncalves-Cayas
Copy link
Author

The TypeConverter should be ThicknessTypeConverter:
TypeConverter={x:Type ThicknessTypeConverter}

@grabnerM
Copy link

Now i get an error again (" Cannot resolve type 'http://schemas.microsoft.com/dotnet/2021/maui:ThicknessTypeConverter'")

<VerticalStackLayout Margin="{ local:OnOrientation Portrait=40, Landscape=20, Default=0, TypeConverter={x:Type ThicknessTypeConverter} }">
some labels
</VerticalStackLayout>

Am i doing something wrong?

@FlavioGoncalves-Cayas
Copy link
Author

You need to add the namespace
xmlns:converters="clr-namespace:Microsoft.Maui.Converters;assembly=Microsoft.Maui"
and then change it to
TypeConverter={x:Type converters:ThicknessTypeConverter}

@grabnerM
Copy link

Now this works. Thank you very much

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment