Contributing to the Uno Platform (part 2)

Welcome to the second part of the journey towards our first Uno Platform contribution. In the first part of this series we started by looking at Uno as a whole, had a first look at the codebase. This time we will look at the current implementation of DisplayInformation class, update the samples app to have some UI to test our code on, meet the UWPSyncGenerator and implement ScreenWidthInRawPixels and ScreenHeightInRawPixels.

Uno Platform

The DisplayInformation class

As I mentioned, the issue I attempt to fix has number #385 – “Add support for DisplayInformation Dimensions.

First of all let’s look at what exactly the UWP’s DisplayInformation class is. Located in the Windows.Graphics.Display namespace, this class provides (unsurprisingly) information about the display the app is running on. In addition, the class also provides a few events, which notify about display changes like OrientationChanged.

To implement the APIs, it is vital to match the UWP behavior as much as possible. Thankfully, the Microsoft Docs are very well written and each property is an adequate description which can be used as a guideline.

I decided to implement all DisplayInformation properties for Android and iOS, leaving other platforms and events for a future PR. My reasoning is to keep the pull request focused and have faster feedback on the code and my approach. Once it is validated, I can continue with more confidence.

Existing Uno implementation

When I browsed into the Uno project and its Graphics/Display subfolder, I noticed DisplayInformation.cs as well as DisplayInformation.Android.cs and DisplayInformation.iOS.cs files were already present with the implementation of the AutoRotationPreferences , CurrentOrientation and OrientationChanged on iOS and AutoRotationPreferences on Android.

The typical way to get an instance of DisplayInformation in UWP is via the GetForCurrentView method. On UWP this returns an instance for the current UI thread but Uno currently uses a simpler approach creating a singleton irrespectively of the calling thread:

public static DisplayInformation GetForCurrentView()
{
if (_instance == null)
{
_instance = new DisplayInformation();
}
return _instance;
}

The parameterless constructor is marked as private so new instances cannot be created. The constructor calls to a partial Initialize method.

private DisplayInformation()
{
Initialize();
}
partial void Initialize();

The Initialize method is implemented in a platform-specific manner in the DisplayInformation.Android.cs and DisplayInformation.iOS.cs files.

As an example, on iOS the Initialize method calls InitializeCurrentOrientation which sets up the initial CurrentOrientation value:

private void InitializeOrientation()
{
_didChangeStatusBarOrientationObserver = NSNotificationCenter
.DefaultCenter
.AddObserver(
UIApplication.DidChangeStatusBarOrientationNotification,
n => {
UpdateCurrentOrientation();
OrientationChanged?.Invoke(this, CurrentOrientation);
}
);
UpdateCurrentOrientation();
}
private void UpdateCurrentOrientation()
{
var currentOrientationMask = UIApplication.SharedApplication
.StatusBarOrientation;
switch (currentOrientationMask)
{
case UIInterfaceOrientation.LandscapeLeft:
CurrentOrientation = DisplayOrientations.LandscapeFlipped;
break;
case UIInterfaceOrientation.LandscapeRight:
CurrentOrientation = DisplayOrientations.Landscape;
break;
case UIInterfaceOrientation.Portrait:
CurrentOrientation = DisplayOrientations.Portrait;
break;
case UIInterfaceOrientation.PortraitUpsideDown:
CurrentOrientation = DisplayOrientations.PortraitFlipped;
break;
}
NativeOrientation = CurrentOrientation;
}

As you can see, StatusBar orientation is used to determine the current UI orientation, as that is the most reliable way.

Updating the sample app

To make implementation easier to test, I created a simple test user control in the SamplesApp:

<UserControl
x:Class="UITests.Shared.Windows_Graphics_Display.DisplayInformation.DisplayInformation_Properties"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button Content="Refresh" Click="Refresh_Click" />
<ListView x:Name="PropertyListView" Grid.Row="1">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Padding="4">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Text="{Binding Value}" Grid.Row="1" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</UserControl>

The control hosts a ListView which will display the DisplayInformation property values and a Button to refresh them.

The code-behind marks the UserControl with a  SampleControlInfoAttribute which is used to discover and categorize the samples.

namespace UITests.Shared.Windows_Graphics_Display.DisplayInformation
{
[SampleControlInfo("DisplayInformation", "DisplayInformation_Properties", description: "Shows the values from DisplayInformation class properties. N/A for not implemented.")]
public sealed partial class DisplayInformation_Properties : UserControl
{
public DisplayInformation_Properties()
{
this.InitializeComponent();
RefreshDisplayInformation();
}
public class PropertyInformation
{
public PropertyInformation( string name, string value)
{
Name = name;
Value = value;
}
public string Name { get; set; }
public string Value { get; set; }
}
private void Refresh_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
RefreshDisplayInformation();
}

The RefreshDisplayInformation method’s purpose is to retrieve the current property values from DisplayInformation and display them. My original plan was to use reflection to get the values:

private void RefreshDisplayInformation()
{
var info = Windows.Graphics.Display.DisplayInformation.GetForCurrentView();
var type = info.GetType();
var typeInfo = type.GetTypeInfo();
var propertyInfos = typeInfo.GetProperties();
var properties = propertyInfos.Select(p => new PropertyInformation() { Name = p.Name, Value = SafeGetValue(p, info) }).ToArray();
PropertyListView.ItemsSource = properties;
}

That worked pretty well until I tested the code on iOS where most of the properties in the list were missing. The reason for that is the fact the linker strips out unused code to make the assembly as small as possible and the new DisplayInformation properties which were never actually accessed were removed. I fixed the code by hardcoding the access to the properties in the sample:

private void RefreshDisplayInformation()
{
var info = DisplayInfo.GetForCurrentView();
var properties = new PropertyInformation[]
{
new PropertyInformation(nameof(DisplayInfo.AutoRotationPreferences), SafeGetValue(()=>DisplayInfo.AutoRotationPreferences)),
new PropertyInformation(nameof(info.CurrentOrientation), SafeGetValue(()=>info.CurrentOrientation)),
new PropertyInformation(nameof(info.NativeOrientation), SafeGetValue(()=>info.NativeOrientation)),
new PropertyInformation(nameof(info.ScreenHeightInRawPixels), SafeGetValue(()=>info.ScreenHeightInRawPixels)),
new PropertyInformation(nameof(info.ScreenWidthInRawPixels), SafeGetValue(()=>info.ScreenWidthInRawPixels)),
new PropertyInformation(nameof(info.LogicalDpi), SafeGetValue(()=>info.LogicalDpi)),
new PropertyInformation(nameof(info.DiagonalSizeInInches), SafeGetValue(()=>info.DiagonalSizeInInches)),
new PropertyInformation(nameof(info.RawPixelsPerViewPixel), SafeGetValue(()=>info.RawPixelsPerViewPixel)),
new PropertyInformation(nameof(info.RawDpiX), SafeGetValue(()=>info.RawDpiX)),
new PropertyInformation(nameof(info.RawDpiY), SafeGetValue(()=>info.RawDpiY)),
new PropertyInformation(nameof(info.ResolutionScale), SafeGetValue(()=>info.ResolutionScale)),
};
PropertyListView.ItemsSource = properties;
}

Granted, this lacks the simplicity and robustness of the first solution, but it works.

For completeness, I also include the code of the SafeGetValue method, which uses the passed-in Func<T> to get the property value and converts it to string if possible.

private string SafeGetValue<T>(Func<T> getter)
{
try
{
var value = getter();
if ( value == null)
{
return "(null)";
}
return Convert.ToString(value);
}
catch (NotImplementedException ex)
{
return "(Not implemented)";
}
}

view raw
SafeGetValue.cs
hosted with ❤ by GitHub

Note the code handles the NotImplementedException which is the default for APIs not implemented by Uno.

This is the sample running on Windows against the UWP APIs. Lots to implement!

DisplayInformation page in SamplesApp
DisplayInformation page in SamplesApp

Staying in sync with the UWP API

Now it’s time for yet another part of the Uno magic. How does the team keep the API in sync with UWP? With feature updates to Windows 10 coming twice a year, it would be daunting to go through every new and changed API and ensure they are 1:1. This is where the Uno.UWPSyncGenerator project comes into the picture.

First, we have to understand how are the “not implemented” APIs handled in Uno codebase. Within the Uno and Uno.UI projects you can find folders with the named Generated . In there you can find all the namespaces of the UWP API with a separate .cs file per type. For example DisplayInformation.cs is in the Generated/3.0.0.0/Windows.Graphics.Display subfolder and contains the following:

#pragma warning disable 108 // new keyword hiding
#pragma warning disable 114 // new keyword hiding
namespace Windows.Graphics.Display
{
#if false || false || false || false || false
[global::Uno.NotImplemented]
#endif
public partial class DisplayInformation
{
// Skipping already declared property CurrentOrientation
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public float LogicalDpi
{
get
{
throw new global::System.NotImplementedException("The member float DisplayInformation.LogicalDpi is not implemented in Uno.");
}
}
#endif
// Skipping already declared property NativeOrientation
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public float RawDpiX
{
get
{
throw new global::System.NotImplementedException("The member float DisplayInformation.RawDpiX is not implemented in Uno.");
}
}
#endif
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public float RawDpiY
{
get
{
throw new global::System.NotImplementedException("The member float DisplayInformation.RawDpiY is not implemented in Uno.");
}
}
#endif
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public global::Windows.Graphics.Display.ResolutionScale ResolutionScale
{
get
{
throw new global::System.NotImplementedException("The member ResolutionScale DisplayInformation.ResolutionScale is not implemented in Uno.");
}
}
#endif
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public bool StereoEnabled
{
get
{
throw new global::System.NotImplementedException("The member bool DisplayInformation.StereoEnabled is not implemented in Uno.");
}
}
#endif
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public double RawPixelsPerViewPixel
{
get
{
throw new global::System.NotImplementedException("The member double DisplayInformation.RawPixelsPerViewPixel is not implemented in Uno.");
}
}
#endif
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public double? DiagonalSizeInInches
{
get
{
throw new global::System.NotImplementedException("The member double? DisplayInformation.DiagonalSizeInInches is not implemented in Uno.");
}
}
#endif
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public uint ScreenHeightInRawPixels
{
get
{
throw new global::System.NotImplementedException("The member uint DisplayInformation.ScreenHeightInRawPixels is not implemented in Uno.");
}
}
#endif
#if __ANDROID__ || __IOS__ || NET46 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public uint ScreenWidthInRawPixels
{
get
{
throw new global::System.NotImplementedException("The member uint DisplayInformation.ScreenWidthInRawPixels is not implemented in Uno.");
}
}
#endif
// Skipping already declared property AutoRotationPreferences
//… and more!
}
}

This is all auto-generated code with a NotImplementedException wrapper for everything which is not implemented yet. Also note all the classes are partial. The actual classes implementing the APIs are partial as well.

The Uno.UWPSyncGenerator takes care of generating all this code and is quite smart about it as well. It uses the Microsoft.CodeAnalysis NuGet package and goes through the Uno and Uno.UI projects to check what is already declared for which platforms.

For example, I add my first three DisplayInformation properties in the DisplayInformation.cs file in Uno project:

public uint ScreenHeightInRawPixels { get; private set; }
public uint ScreenWidthInRawPixels { get; private set; }
public float LogicalDpi { get; private set; }

After running the UWPSyncGenerator the declared properties disappear from the Generated folder and are replaced with an informative comment:

// Skipping already declared property LogicalDpi
// Skipping already declared property ScreenHeightInRawPixels
// Skipping already declared property ScreenWidthInRawPixels

But that’s not all! What if I decided to implement a property only for Android for example?

#if __ANDROID__
public uint ScreenHeightInRawPixels { get; private set; }
#endif

The tool is totally able to handle this and generates:

#if false || __IOS__ || NET461 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public uint ScreenHeightInRawPixels
{
get
{
throw new global::System.NotImplementedException("The member uint DisplayInformation.ScreenHeightInRawPixels is not implemented in Uno.");
}
}

Et voilà, the #if directive now contains a harmless false instead of __ANDROID__ symbol.

Implementing ScreenWidthInRawPixels and ScreenHeightInRawPixels

I won’t dive too much into the specifics of implementing all the DisplayInformation mostly because it is platform specific code and this series of articles is devoted to the process of contributing to Uno itself. But to demonstrate the process in general, we can use the example of ScreenWidthInRawPixels and ScreenHeightInRawPixels.

These properties should return the actual resolution of the display in pixels. First, I add the properties to the platform-agnostic DisplayInformation.cs file:

public uint ScreenHeightInRawPixels { get; private set; }
public uint ScreenWidthInRawPixels { get; private set; }

view raw
ScreenSize.cs
hosted with ❤ by GitHub

This will generate a conflict with the NotImplemented properties in the Generated folder, but running UWPSyncGenerator fixes this.

Now let’s assign these properties some useful values!

iOS

I have added a new UpdateProperties method which is called by the Initialize method in DisplayInformation.iOS.cs:

partial void Initialize()
{
InitializeOrientation();
UpdateProperties();
}

view raw
InitializeiOS.cs
hosted with ❤ by GitHub

I will use the UpdateProperties method to reset the property values when the screen changes. For now, I am not implementing the events though, so I am just calling it during initialization. To make the code clear, I separate the UpdateProperties method into logical groups.

private void UpdateProperties()
{
UpdateRawProperties();
}
private void UpdateRawProperties()
{
}

The screen size in iOS can be retrieved from the UIScreen.MainScreen.Bounds.Size property. This, however, gives us the number of layout “points” the screen has. Everything is then actually rendered with UIScreen.MainScreen.Scale scaling and scaled down to the native device resolution. This infographic is very helpful to understand the different screen “sizes”. Luckily there is a UIScreen.MainScreen.NativeScale property which reports the “native scale” required to get the actual screen size:

private void UpdateRawProperties()
{
var screenSize = UIScreen.MainScreen.Bounds.Size;
var scale = UIScreen.MainScreen.NativeScale;
ScreenHeightInRawPixels = (uint)(screenSize.Height * scale);
ScreenWidthInRawPixels = (uint)(screenSize.Width * scale);
}

view raw
ScreenSize.iOS.cs
hosted with ❤ by GitHub

The sample app now properly shows:

Raw screen size on iOS
Raw screen size on iOS

Android

On Android I tried to follow a similar structure to iOS:

partial void Initialize()
{
UpdateProperties();
}
private void UpdateProperties()
{
var realDisplayMetrics = new DisplayMetrics();
var windowManager = ContextHelper.Current.GetSystemService(Context.WindowService).JavaCast<IWindowManager>();
windowManager.DefaultDisplay.GetRealMetrics(realDisplayMetrics);
UpdateRawProperties(realDisplayMetrics);
}

There is a bit of additional code retrieving the display metrics and IWindowManager, as those will be reused to implement more properties later. The basic UpdateRawProperties implementation looks like this:

private void UpdateRawProperties(DisplayMetrics realDisplayMetrics)
{
ScreenWidthInRawPixels = (uint)realDisplayMetrics.WidthPixels;
ScreenHeightInRawPixels = (uint)realDisplayMetrics.HeightPixels;
}

DisplayMetrics we retrieved above directly offer the raw screen width and height in pixels, so we just cast them and get our desired result:

Native screen size running on my OnePlus 6T
Native screen size running on my OnePlus 6T

Summary

We have checked out the existing DisplayInformation implementation in Uno, understood how the framework keeps its source codes in sync with UWP and implemented our first two properties.

Next time we will briefly mention the most interesting bits from the rest of the implementation including handling of the OrientationChanged event on Android. Then we finally create our pull request a submit it for review!

Buy me a coffeeBuy me a coffee

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.