Improving Developer Experience with Multi-Targeted Visual Studio Projects

In my previous post on using multi-targeted cross platform projects I showed how you can add additional target frameworks to allow the addition of platform specific code to a library. One of the downsides of this approach (versus perhaps using a shared project) is that the library gets built once for every target framework that’s … Continue reading “Improving Developer Experience with Multi-Targeted Visual Studio Projects”

In my previous post on using multi-targeted cross platform projects I showed how you can add additional target frameworks to allow the addition of platform specific code to a library. One of the downsides of this approach (versus perhaps using a shared project) is that the library gets built once for every target framework that’s specified. In this post I’m going to show you how you can use the combination of a simple script and Solution Filters to optimise both the time it takes to load your solution in Visual Studio and the time it takes to compile your application.

Firstly, we’re going to adjust the Core project so that we can adjust which target frameworks are include based on a solution property called TargetsToBuild. If this is set to All, we’ll simply use the TargetFrameworks we defined in the previous post (i.e. .NET Standard, iOS, Android and Windows (UAP) when building on Windows). However, if TargetsToBuild is not equal to All, we’ll set the TargetFrameworks to be just the specific target framework that we’re interested in.

<Project Sdk="MSBuild.Sdk.Extras">
	<PropertyGroup Condition=" '$(TargetsToBuild)' == 'All' ">
		<TargetFrameworks>netstandard2.0;xamarinios10;monoandroid10.0;</TargetFrameworks>
		<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">uap10.0.16299;$(TargetFrameworks)</TargetFrameworks>
	</PropertyGroup>

	<PropertyGroup Condition=" '$(TargetsToBuild)' != 'All' ">
		<TargetFrameworks Condition=" '$(TargetsToBuild)' == 'Android' ">monoandroid10.0;</TargetFrameworks>
		<TargetFrameworks Condition=" '$(TargetsToBuild)' == 'Windows' ">uap10.0.16299</TargetFrameworks>
		<TargetFrameworks Condition=" '$(TargetsToBuild)' == 'iOS' ">xamarinios10</TargetFrameworks>
	</PropertyGroup>

Note: I quite often will include netstandard2.0 in all of the TargetFrameworks to make sure the .NET Standard 2.0 build doesn’t break as we’re changing the Core library. This is a personal choice and you’ll definitely get better performance out of not having it build every time.

Ok, so the question is, where does TargetsToBuild get set. For this, we’re going to include a file called Directory.build.props in the solution folder. You can think of this as a file where you can define properties and include items and references across the entire solution. In this case, we’re going to set the contents to the following.

<Project>
  <PropertyGroup>
    <TargetsToBuildDeveloperOverride>All</TargetsToBuildDeveloperOverride>                   
  </PropertyGroup>

  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
    <TargetsToBuild>$(TargetsToBuildDeveloperOverride)</TargetsToBuild> 
  </PropertyGroup>

  <PropertyGroup Condition=" '$(Configuration)' != 'Debug' ">
    <TargetsToBuild>All</TargetsToBuild>
  </PropertyGroup>
</Project>

There’s two parts to this:

  • Firstly, if the Configuration is not set to Debug, the TargetsToBuild is always set to All. This is never modified, and is a fail safe to ensure all target frameworks are built when doing a Release build.
  • Secondly, the TargetsToBuild is set to the value held by TargetsToBuildDeveloperOverride when the Configuration is Debug. This variable is currently set to All.

The property TargetsToBuildDeveloperOverride essentially has four possible values: All, Android, iOS and Windows. Changing this value will adjust the target frameworks for the Core project. I would recommend restarting Visual Studio as the support for dynamically switching target frameworks in any given session isn’t there yet and doesn’t work 100% reliably.

The next piece to the puzzle is to use Solution Filters to limit which projects are loaded. By limiting which projects are loaded, your solution will load quicker and will also build faster (depending on what your build configuration settings are). To create a Solution Filter, simply unload the projects you don’t want loaded, then right-click on the solution node in Solution Explorer and click Save as Solution Filter. I’ve created the following solution filters:

  • MultiTargetSample.all.slnf: All projects are loaded
  • MultiTargetSample.Android.slnf: Core and Android head project loaded
  • MultiTargetSample.iOS.slnf: Core and iOS head project loaded
  • MultiTargetSample.Windows.slnf: Core and Windows head project loaded

The solution filter file, SLNF, is just a JSON file that lists which projects should be loaded. For example the MultiTargetSample.Windows.slnf file is as follows:

{
  "solution": {
    "path": "MultiTargetSample.sln",
    "projects": [
      "MultiTarget.Core\\MultiTarget.Core.csproj",
      "MultiTarget.Windows\\MultiTarget.Windows.csproj"
    ]
  }
}

The last piece of the puzzle is to combine the solution filters with the TargetsToBuildDeveloperOverride property. For example, if we want to be working on the Android application, we want to set the TargetsToBuildDeveloperOverride property to Android and then load the MultiTargetSample.Android.slnf solution filter. This is easily done using a couple of simple batch script.

Firstly, a generic script (LaunchVS.bat) that takes a parameter (the platform) and adjusts the TargetsToBuildDeveloperOverride property and then launches the appropriate solution filter.

powershell -Command "(gc Directory.build.props) -replace '[a-zA-Z]*', '%~1 ' | Out-File -encoding ASCII Directory.build.props"
start MultiTargetSample.%~1.slnf

Then, we have a script for each platform (eg LaunchVS.Windows.bat) which simply has the following command

LaunchVS.bat Windows

When you double-click on LaunchVS.Windows.bat in File Explorer, Visual Studio is launched with only the Core and Windows projects loaded. The Core library will only build the UAP target framework. This significantly improves both the solution load time and the build time, making working with MultiTargeted projects much easier.

Important: You might think that this isn’t important when you’re first starting out but I would highly recommend adding this to your solution from the get-go. It will save you hours of time both now and when your solution grows. Visual Studio does not handle large solutions with projects that have many target frameworks. Applying these changes will ensure Visual Studio continues to operate properly

Cross-Platform Libraries with Multi-Targeting for Xamarin, Uno and Windows Development

The story of cross platform development for Microsoft developers has been one marked by iteration after iteration of .NET framework fragmentation. With .NET 5 the promise is that we’ll return to having a single framework that will be used everywhere…. Sorry, what?? when did we ever have a single framework? For as long as I … Continue reading “Cross-Platform Libraries with Multi-Targeting for Xamarin, Uno and Windows Development”

The story of cross platform development for Microsoft developers has been one marked by iteration after iteration of .NET framework fragmentation. With .NET 5 the promise is that we’ll return to having a single framework that will be used everywhere…. Sorry, what?? when did we ever have a single framework? For as long as I can recall, developers working on mobile and/or IoT have always had a different framework to target (.NET CF, .NET MF, WinPhone (whatever that .NET version was called), UWP etc). However, one thing that hasn’t changed is the fact that in order to target different platforms, you inevitably have to write code that’s specific to that platform. Even during the announcement of MAUI, the new single project structure has specific files/code etc for iOS, Android, Windows etc. This problem isn’t going away, so let’s discuss how we deal with this issue today.

The scenario I’m going to cover in the post is relatively simple. I have three “head” or “target platform” projects (Windows, Android, iOS) that are going to reference a common library (Core). Whilst most of the Core library is common code that is compatible with .NET Standard 2.0, there’s some code that is specific to each platform. Rather than create a separate platform specific library, we can include this specific logic by setting up the Core library to be multi-targeted – meaning that a different DLL is generated for each supported platform. Luckily Visual Studio knows how to handle this, so you’re not left working out how to reference each DLL from the head projects.

Project Setup

Let’s get started. First up, we’ll create a blank solution. If you start with one of the head projects your solution will end up being called something list XXX.Windows.

Search for Solution and select Blank Solution
Name your solution

Next up, let’s create the three head projects: Windows (UWP), Android and iOS

Search for UWP and select the Blank App (Universal Windows) template
Name you Windows head project – I like to use the Windows suffix but uwp is also often used
Search for droid and select the Android App (Xamarin) template
Name your Android head project
Search for iOS and select the iOS App (Xamarin) template
Name your iOS head project

At this point, if you’re working on a Mac, you could also add a MacOS project template. For some reason this template isn’t available in Visual Studio for Windows (because no developer would ever want to create the MacOS project when working on windows …. I guess the same (lack of) logic is used on VS for Mac where you can’t create a UWP project).

Next up, we’ll create the common library that each of the head projects will reference.

Search for .net standard and select the Class Library (.NET Standard) template
Name the .NET Standard library

Common Code

So that we can see how this library will work, we’ll simply expose a string constant that can be referenced and displayed in each head project. We’ll rename the Class1.cs file to AppProperties.cs and allow Visual Studio to rename the corresponding class. We’ll then add a string constant as follows:

namespace MultiTarget.Core
{
    public class AppProperties
    {
        public const string AppName = "MultiTarget Sample";
    }
}

In each of the head projects we’ll add an text/label element to the UI and set the value to be AppProperties.AppName. Rather than having to use the Add Reference workflow (i.e. right click on the head project and click Add Reference), we’ll simply type the AppProperties class name and use the prompt in the code editor to Add reference to the MultiTarget.Core project.

Type Ctrl+. to get the code editor suggestions to appear.

Now we can run each of the head projects and see the same output on the screen which should say “MultiTarget Sample” (I don’t have an iOS device/simulator handy, so I don’t have a screenshot of it running on iOS).

Android – MultiTarget Sample
Windows – MultiTarget Sample

Platform Specific Code

The next step is to add platform specific code. In this case we’re going to augment the existing AppProperties class by adding a platform specific partial class. In Solution Explorer we can copy the AppProperties.cs and paste it into the same project. The copied file can then be renamed based on the platform. For example (see image below) the Windows specific code would be in a file called AppProperties.windows.cs.

Note: The actual name of the platform specific files doesn’t have any intrinsic meaning. As we’ll see in a minute, we will use the platform specific suffix to define which files will be included for each platform.

Inside the platform specific file we’ll add another constant called PlatformName. For example, the AppProperties.windows.cs includes a PlatformName constant that returns “Windows.”

using System;

namespace MultiTarget.Core
{
    public partial class AppProperties
    {
        public const string PlatformName = "Windows";
    }
}

We’re also going to update the AppName constant in the AppProperties.cs file to include the PlatformName. We also needed to add the partial keyword to the class definition.

namespace MultiTarget.Core
{
    public partial class AppProperties
    {
        public const string AppName = "MultiTarget Sample - " + PlatformName;
    }
}

After adding files for each platform, if you attempt to build the Core project it will fail with an error indicating that there are multiple definitions of the PlatformName constant. What we need to do is to only include one platform specific file at a time.

MultiTarget Project

Currently the Core project is configured with a single TargetFramework, netstandard2.0. This means that the only API that can be accessed are those defined by netstandard2.0. If we want to include platform specific code, or want to access platform specific APIs, we need to change the project to target multiple frameworks. Here’s the current project XML for the Core library

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
</Project>

In order to target multiple frameworks, we can just change the TargetFramework element to TargetFrameworks, and add multiple frameworks, semicolon delimited (eg <TargetFrameworks>netstandard2.0;net451</TargetFrameworks> ). However, out of the box, this doesn’t support the various target framework monikers that are used across different platforms. To overcome this limitation, we need to change the Sdk attribute from “Microsoft.NET.Sdk” to “MSBuild.Sdk.Extras”. We also need to create a file called global.json in the solution folder with the following version information for the MSBuild.Sdk.Extra nuget package:

{
    "msbuild-sdks": {
        "MSBuild.Sdk.Extras": "2.0.54"
    }
}

Note: Alternatively you can specify the version number in the Sdk attribute (eg Sdk=”MSBuild.Sdk.Extras/2.0.54″ ).

With these changes made, we can update the project file to include multiple target frameworks:

<Project Sdk="MSBuild.Sdk.Extras">
	<PropertyGroup>
		<TargetFrameworks>netstandard2.0;xamarinios10;monoandroid10.0;</TargetFrameworks>
		<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">uap10.0.16299;$(TargetFrameworks)</TargetFrameworks>
	</PropertyGroup>
</Project>

Note: we’ve included a conditional TargetFrameworks element so that the Windows (i.e. UWP) target platform is only include if the host OS is Windows. This is important because the UWP platform can’t be built using Visual Studio for Mac, so making this target framework conditional on the OS ensures the project can be loaded and built using Visual Studio for Mac.

At this point it’s worth noting that by including additional target frameworks, you’re effectively increasing the number of steps in the build. For example, when we build the Core library, we see that four DLLs are built – effectively increasing the built time by four times!!!

The new project format includes all files nested in the project folder by default. This means that we need to change the default behaviour for the platform specific files so that they’re excluded by default. Then we can specifically include files for each platform that is built. The project file now looks like this:

<Project Sdk="MSBuild.Sdk.Extras">
	<PropertyGroup>
		<TargetFrameworks>netstandard2.0;xamarinios10;monoandroid10.0;</TargetFrameworks>
		<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">uap10.0.16299;$(TargetFrameworks)</TargetFrameworks>
	</PropertyGroup>

	<ItemGroup>
		<Compile Remove="**\*.netstandard.cs" />
		<Compile Remove="**\*.droid.cs" />
		<Compile Remove="**\*.ios.cs" />
		<Compile Remove="**\*.windows.cs" />
	</ItemGroup>

	<ItemGroup Condition=" $(TargetFramework.StartsWith('netstandard')) ">
		<Compile Include="**\*.netstandard.cs" />
	</ItemGroup>

	<ItemGroup Condition=" $(TargetFramework.StartsWith('xamarinios')) ">
		<Compile Include="**\*.ios.cs" />
	</ItemGroup>

	<ItemGroup Condition=" $(TargetFramework.StartsWith('monoandroid')) ">
		<Compile Include="**\*.droid.cs" />
	</ItemGroup>

	<ItemGroup Condition=" $(TargetFramework.StartsWith('uap')) ">
		<Compile Include="**\*.windows.cs" />
	</ItemGroup>
</Project>

And that’s it – building the Core library will result in four DLLs, each one having a different value for PlatformName, and thus AppName. We didn’t need to make any changes to the head projects.

This technique for including platform specific code in a common library is used by popular frameworks such as ReactiveUI and MvvmCross, as well as Microsoft libraries such as Xamarin Essentials.

Source code

Microsoft Build 2020: Uno Platform + WinUI = Future of Cross Platform Apps

I’ve been a long time supporter of the work that the team at Uno Platform have been doing. The announcement today of v3.0 of the Uno Platform comes hot on the heals of releases 2.3 and 2.4 that introduced AndroidX and MacOS support. This isn’t just any old announcement, v3 aligns the Uno Platform with … Continue reading “Microsoft Build 2020: Uno Platform + WinUI = Future of Cross Platform Apps”

I’ve been a long time supporter of the work that the team at Uno Platform have been doing. The announcement today of v3.0 of the Uno Platform comes hot on the heals of releases 2.3 and 2.4 that introduced AndroidX and MacOS support. This isn’t just any old announcement, v3 aligns the Uno Platform with the preview of WinUI 3.

At Built to Roam we’ve been building XAML based applications across a variety of technologies: WPF, Silverlight, WinPhone/Win8, UWP. Each iteration of Windows 10, new features were added to UWP but this limited the reach of the application to devices running the latest version of Windows 10. With WinUI 3.0 Microsoft is detaching the XAML platform away from the Windows version – this means that WinUI can iterate faster and bring newer controls and features to developers. In addition, WinUI also brings with it support for Win32 based applications – more on WinUI here.

So, why is it important that Uno Platform supports WinUI 3.0? Well, if you think of WinUI extending UWP to support all Windows 10 devices, then the addition of the Uno Platform takes WinUI across iOS, Android, macOS and Web via WebAssembly #WinUIEverywhere.

For more information, check out the Announcement of Uno Platform 3.0

Shadows in Windows (UWP) XAML Applications – Part 4 – Custom Shadows

In part 2 of this series of posts on Shadows in Windows (UWP) XAML Applications (parts 1, 1b, 2 and 3) we saw that the composition APIs could be used to generate a DropShadow. However, what wasn’t immediately clear is that this mechanism only works for a limited set of controls, namely Shape (including Ellipse, … Continue reading “Shadows in Windows (UWP) XAML Applications – Part 4 – Custom Shadows”

In part 2 of this series of posts on Shadows in Windows (UWP) XAML Applications (parts 1, 1b, 2 and 3) we saw that the composition APIs could be used to generate a DropShadow. However, what wasn’t immediately clear is that this mechanism only works for a limited set of controls, namely Shape (including Ellipse, Line, Path, Polygon, Polyline, Rectangle), Image and TextBlock. This is because these controls are the only ones that expose the GetAlphaMask method (and frustratingly this method isn’t even part of a common interface that the controls share). This challenge has previously been pointed out by Mike Taulty in his post about creating shadows, back in 2016. I’m still amazed that there doesn’t seem to be any improvement on this over 4 years later (and no, the ThemeShadow, as I pointed out in my post, isn’t the solution to this problem).

So how do we create a shadow for elements that don’t have the GetAlphaMask? Well the answer presented by Mike was to generate an image from the element, and then use a CompositionBrush generated from the image in order to define the mask for the DropShadow. To do this, we’ll start by adding the CompositionImageBrush to our application (code taken from Mike’s post).

public class CompositionImageBrush : IDisposable
{
    CompositionGraphicsDevice graphicsDevice;
    CompositionDrawingSurface drawingSurface;
    CompositionSurfaceBrush drawingBrush;

    public CompositionBrush Brush => drawingBrush;

    private CompositionImageBrush()
    {
    }

    private void CreateDevice(Compositor compositor)
    {
        graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(
            compositor, CanvasDevice.GetSharedDevice());
    }

    private void CreateDrawingSurface(Size drawSize)
    {
        drawingSurface = graphicsDevice.CreateDrawingSurface(
            drawSize,
            DirectXPixelFormat.B8G8R8A8UIntNormalized,
            DirectXAlphaMode.Premultiplied);
    }

    private void CreateSurfaceBrush(Compositor compositor)
    {
        drawingBrush = compositor.CreateSurfaceBrush(drawingSurface);
    }

    public static CompositionImageBrush FromBGRASoftwareBitmap(
        Compositor compositor,
        SoftwareBitmap bitmap,
        Size outputSize)
    {
        CompositionImageBrush brush = new CompositionImageBrush();

        brush.CreateDevice(compositor);

        brush.CreateDrawingSurface(outputSize);
        brush.DrawSoftwareBitmap(bitmap, outputSize);
        brush.CreateSurfaceBrush(compositor);

        return (brush);
    }

    private void DrawSoftwareBitmap(SoftwareBitmap softwareBitmap, Size renderSize)
    {
        using (var drawingSession = CanvasComposition.CreateDrawingSession(drawingSurface))
        using (var bitmap = CanvasBitmap.CreateFromSoftwareBitmap(drawingSession.Device, softwareBitmap))
        {
            drawingSession.DrawImage(bitmap,
                new Rect(0, 0, renderSize.Width, renderSize.Height));
        }
    }
        
    public void Dispose()
    {
        drawingBrush.Dispose();
        drawingSurface.Dispose();
        graphicsDevice.Dispose();
    }
}

Next, we’re going to create an extension method for UIElement that will either return the result from GetAlphaMask for those elements where it’s defined, or it will return a brush generated from taking an image shapshot of the element.

public static class UIElementHelpers
{
    public static async Task<CompositionBrush> ShadowAlphaMask(this UIElement uiElement)
    {
        CompositionBrush mask = null;
        if (uiElement is Shape shapeElement)
        {
            mask = shapeElement.GetAlphaMask();
        }
        else if (uiElement is Image imageElement)
        {
            mask = imageElement.GetAlphaMask();
        }
        else if (uiElement is TextBlock textElement)
        {
            mask = textElement.GetAlphaMask();
        }
        else if (uiElement is FrameworkElement frameworkElement)
        {
            var gridVisual = ElementCompositionPreview.GetElementVisual(uiElement);
            var elementVisual = gridVisual.Compositor.CreateSpriteVisual();
            elementVisual.Size = uiElement.RenderSize.ToVector2();
            var bitmap = new RenderTargetBitmap();
            await bitmap.RenderAsync(
                uiElement,
                (int)frameworkElement.ActualWidth,
                (int)frameworkElement.ActualHeight);
            var pixels = await bitmap.GetPixelsAsync();
            using (var softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(
                pixels,
                BitmapPixelFormat.Bgra8,
                bitmap.PixelWidth,
                bitmap.PixelHeight,
                BitmapAlphaMode.Premultiplied))
            {
                var brush = CompositionImageBrush.FromBGRASoftwareBitmap(
                    gridVisual.Compositor,
                    softwareBitmap,
                    new Size(bitmap.PixelWidth, bitmap.PixelHeight));
                mask = brush.Brush;
            }
        }
        return mask;
    }
}

Lastly we need to modify the code for generating the DropShadow to use this new extension method.

private async void Grid_Loaded(object sender, RoutedEventArgs e)
{
    var shadowColor = (Resources["ApplicationForegroundThemeBrush"] as SolidColorBrush).Color;
    var compositor = ElementCompositionPreview.GetElementVisual(Host).Compositor;

    // Create the drop shadow
    var dropShadow = compositor.CreateDropShadow();
    dropShadow.Color = shadowColor;
    dropShadow.BlurRadius = 16;
    dropShadow.Opacity = 20.0f;

    // Use the shape of the element (in this case ShadowContent) to 
    // control shape of shadow
    var mask = await ShadowContent.ShadowAlphaMask();
    dropShadow.Mask = mask;

    // Set the shadow on the visual
    var spriteVisual = compositor.CreateSpriteVisual();
    spriteVisual.Size = new Vector2((float)Host.ActualWidth, (float)Host.ActualHeight);
    spriteVisual.Shadow = dropShadow;
    ElementCompositionPreview.SetElementChildVisual(Host, spriteVisual);
}

Previously we were generating the shadow for an element called Rectangle2 – we’ve updated the code to use ShadowContent and the XAML now looks like.

<Grid Margin="50"
        Height="200"
        Width="200"
        VerticalAlignment="Bottom"
        HorizontalAlignment="Left">
    <Grid x:Name="Host" />
    <StackPanel x:Name="ShadowContent"
                Background="Pink">
        <Rectangle x:Name="Rectangle2"
                    Fill="Turquoise" />
        <TextBox />
        <Button Content="Press me!" />
    </StackPanel>
</Grid>

Ok, so I guess the only thing let to do is to run the application and show you the output.

And there you have it – a nice, easy to use extension method can can return a brush from any element that can be used to mask the DropShadow.

Shadows in Windows (UWP) XAML Applications – Part 3 – DropShadowPanel

In Part 2 of this series I talked about how to create your own shadow using the DropShadow class. I mentioned that whilst it was easy enough to do, the DropShadow had to be created in code, rather than being applied in XAML. Well the good news is that the team building the Windows Community … Continue reading “Shadows in Windows (UWP) XAML Applications – Part 3 – DropShadowPanel”

In Part 2 of this series I talked about how to create your own shadow using the DropShadow class. I mentioned that whilst it was easy enough to do, the DropShadow had to be created in code, rather than being applied in XAML. Well the good news is that the team building the Windows Community Toolkit say the same issue and have produced the DropShadowPanel which can be used to apply a shadow via XAML.

Let’s again use our two Rectangle series. I’m going to jump straight to the XAML that shows how to use the DropShadowPanel, complete with rounded corners and theme colour support.

<controls:DropShadowPanel BlurRadius="10"
                            ShadowOpacity="1"
                            Color="{ThemeResource ApplicationForegroundThemeColor}"
                            Margin="50"
                            VerticalAlignment="Bottom"
                            HorizontalAlignment="Left"
                            IsMasked="True">
    <Rectangle x:Name="Rectangle2"
                Fill="Turquoise"
                RadiusX="40"
                RadiusY="40"
                Height="200"
                Width="200" />
</controls:DropShadowPanel>

It’s worth noting here that the DropShadowPanel accepts a Color for the shadow, rather than a brush. As such I’ve had to register an additional theme resources.

var foregroundBrush = this.Resources["ApplicationForegroundThemeBrush"] as SolidColorBrush;
Resources["ApplicationForegroundThemeColor"] = foregroundBrush.Color;

And then of course we want to see what this looks like in action.

Note that the DropShadowPanel works in both light (left part of image) and dark (right part of image) themes

One last thing to note before I wrap up this post. You’ll notice in the above XAML that there is an attribute IsMasked that is set to True. This is actually the default value for this property, so could be excluded. However, I wanted to make note of it because it can be toggled to adjust the behaviour of the shadow. For example if I set this attribute to false, we see the following shadow being cast.

Clearly, in this scenario we want to set IsMasked to true so that we get the nice rounded corners in the shadow.

For anyone wanting to quickly apply a shadow in XAML, the DropShadowPanel from the Windows Community Toolkit has you covered.

Shadows in Windows (UWP) XAML Applications – Part 2 – DropShadow

Following Part 1 – ThemeShadow (and Part 1b – Lists) in this post we’re going to look at a very simple example of creating your own shadow. I’m going to reuse my simple example of two overlapping rectangles. The goal is to: Add a shadow around the bottom-left rectangle The shadow should elevate the rectangle … Continue reading “Shadows in Windows (UWP) XAML Applications – Part 2 – DropShadow”

Following Part 1 – ThemeShadow (and Part 1b – Lists) in this post we’re going to look at a very simple example of creating your own shadow. I’m going to reuse my simple example of two overlapping rectangles.

The goal is to:

  • Add a shadow around the bottom-left rectangle
  • The shadow should elevate the rectangle off the background
  • The shadow should elevate the rectangle away from the other rectangle
  • The shadow should handle changing the corner radius to allow for rounded corners
  • The shadow should handle changes to the system theme (i.e. dark mode)

Ok, so let’s see how we can achieve this by creating our own DropShadow. As with the ThemeShadow there are two elements that participate in the creation of the shadow effect. There’s the item casting the shadow (in this case Rectangle2) and then there’s the surface where the shadow needs to be rendered. In this case, since we want the shadow to appear around the edge of Rectangle2, we’re going to create an additional Grid (named Host in the following XAML) that matches the size of Rectangle2:

<Grid Height="400"
        Width="400"
        Loaded="Grid_Loaded"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Rectangle x:Name="Rectangle1"
                Margin="50"
                Height="200"
                Width="200"
                Fill="Turquoise"
                VerticalAlignment="Top"
                HorizontalAlignment="Right" />
    <Grid Margin="50"
            Height="200"
            Width="200"
            VerticalAlignment="Bottom"
            HorizontalAlignment="Left">
        <Grid x:Name="Host" />
        <Rectangle x:Name="Rectangle2"
                    Fill="Turquoise" />
    </Grid>
</Grid>

To create and apply the shadow we’re handling the Loaded event on the parent Grid. The following logic creates the DropShadow, uses Rectangle2 as a mask and then attaches the DropShadow to the SpriteVisual for the Host Grid.

private void Grid_Loaded(object sender, RoutedEventArgs e)
{
    var shadowColor = (Resources["ApplicationForegroundThemeBrush"] as SolidColorBrush).Color;
    var compositor = ElementCompositionPreview.GetElementVisual(Host).Compositor;
            
    // Create the drop shadow
    var dropShadow = compositor.CreateDropShadow();
    dropShadow.Color = shadowColor;
    dropShadow.BlurRadius = 16;
    dropShadow.Opacity = 20.0f;

    // Use the shape of the element (in this case Rectangle2) to 
    // control shape of shadow
    var mask = Rectangle2.GetAlphaMask();
    dropShadow.Mask = mask;
            
    // Set the shadow on the visual
    var spriteVisual = compositor.CreateSpriteVisual();
    spriteVisual.Size = new Vector2((float)Host.ActualWidth, (float)Host.ActualHeight);
    spriteVisual.Shadow = dropShadow;
    ElementCompositionPreview.SetElementChildVisual(Host, spriteVisual);
}

The result of this is shown in the following image

Now let’s try rounding the corners of Rectangle2

<Rectangle x:Name="Rectangle2"
            Fill="Turquoise"
            RadiusX="40"
            RadiusY="40" />

Here’s how it looks

This is looking really nice but what about the dark mode support?

Even in dark mode, we’re still seeing the shadow being cast both against the background but also on Rectangle1. Going back to the XAML and code that creates this effect, it’s important to note that the background is set using the ApplicationPageBackgroundThemeBrush and that the color that’s set for the shadow is based on the ApplicationForegroundThemeBrush. Both these brushes are theme aware, meaning that as the device switches between light and dark mode, the brushes have the appropriate color. This means that there’s little else we need to do in order to support dark mode when creating the shadow.

The upshot of creating a shadow this way is that it’s relatively straight forward but does require custom logic to be written – this is less than ideal if you want to apply shadows easily in xaml for example.

In subsequent posts we’ll look at other options for applying shadows that hopefully will mean less custom code.

Shadows in Windows (UWP) XAML Applications – Part 1b – ThemeShadow in Lists

Originally, I had planned that the next post was going to talk about making use of the Shadow property and creating a custom shadow. However, following part 1 there was a bit of dialog online talking about the use of shadows in lists. I figured that it shouldn’t be too hard to extend my example … Continue reading “Shadows in Windows (UWP) XAML Applications – Part 1b – ThemeShadow in Lists”

Originally, I had planned that the next post was going to talk about making use of the Shadow property and creating a custom shadow. However, following part 1 there was a bit of dialog online talking about the use of shadows in lists. I figured that it shouldn’t be too hard to extend my example to use the ThemeShadow for items in a list.

Let’s start with a simple list of 1000 items which we’ll be displaying using a ListView – I’m sure there’s a better way to generate random list items but I was just hacking around, so didn’t feel like engineering anything more fancy.

public IList<string> StaticItems { get; } = BuildStaticItems();

private static List<string> BuildStaticItems()
{
    var list = new List<string>();
    const int max = 1000;
    for (int i = 0; i < max; i++)
    {
        list.Add($"{max} items - {i}");
    }
    return list;
}

Next up we need the XAML layout for the ListView:

<Grid x:Name="RootGrid">
    <Grid x:Name="ParentBackgroundGrid" />

    <ListView ItemContainerStyle="{StaticResource ThousandItemContainerStyle}"
                ItemsSource="{x:Bind StaticItems}">
        <ListView.ItemTemplate>
            <DataTemplate x:DataType="x:String">
                <TextBlock Text="{x:Bind }"
                            Margin="6" />
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</Grid>

There are a couple of things to note here:

  • The ItemsSource is bound to the StaticItems property from the previous code block
  • The ItemTemplate is just a simple TextBlock with a Margin set to indent the text from the edge of the item template.
  • There is a ParentBackgroundGrid priors to the ListView which will render beneath the ListView and will be the surface where the shadows are rendered onto.
  • Lastly this snippet defines an ItemContainerStyle called ThousanItemContainerStyle which we’ll go into more detail in a sec.

Note that in this XAML we haven’t defined either an instance of the ThemeShadow and we haven’t set a background on the items in the list. These will both be defined as part of the ItemContainerStyle, so that we can take advantage of the visual states to adjust the elevation (i.e. the Z axis translate), and thus the shadow cast.

Ok, so here’s the ItemContainerStyle.

<Style x:Key="ThousandItemContainerStyle"
        TargetType="ListViewItem">
    <Setter Property="Padding"
            Value="6" />
    <Setter Property="HorizontalContentAlignment"
            Value="Stretch" />
    <Setter Property="VerticalContentAlignment"
            Value="Stretch" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListViewItem">
                <Grid x:Name="Root">
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Pressed">
                                <VisualState.Setters>
                                    <Setter Target="ContentBackground.Elevation"
                                            Value="0" />
                                </VisualState.Setters>
                            </VisualState>
                            <VisualState x:Name="PressedSelected">
                                <VisualState.Setters>
                                    <Setter Target="ContentBackground.Elevation"
                                            Value="0" />
                                </VisualState.Setters>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <local:ItemBackground Elevation="32"
                                            x:Name="ContentBackground"
                                            ContentPadding="{TemplateBinding Padding}" />
                    <ContentPresenter Margin="{TemplateBinding Padding}" />
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Things to note:

  • I’ve removed a significant portion of the default ItemsContainerStyle you would normally get when you clone the built-in style. What I’m left with is a basic ContentPresenter element and a new UserControl of type ItemBackground.
  • ItemBackground has an initial Elevation of 32 but this property is adjusted when the user presses the button to control the elevation (see the Pressed and PressedSelected visual states), and thus the amount of shadow.

Of course, now we need to look at the ItemsBackground class so we can understand how it’s generating the shadow for each item. The XAML for the ItemBackground UserControl is similar to what we had in my previous post – a root Grid which defines the ThemeShadow resource and includes a Rectangle which is the background of the item. It’s the Rectangle that has the Shadow property set and will be generating the shadow that is to be cast.

<UserControl
    x:Class="ShadowTest.ItemBackground"
    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">
    <Grid Loaded="RootLoaded">
        <Grid.Resources>
            <ThemeShadow x:Name="SharedShadow" />
        </Grid.Resources>
        <Rectangle x:Name="Rectangle2"
                   Fill="Turquoise"
                   Margin="{x:Bind ContentPadding, Mode=OneWay}"
                   Shadow="{StaticResource SharedShadow}" />
    </Grid>
</UserControl>

We’re intercepting the Loaded event on the Grid in order to connect the ThemeShadow with the corresponding Grid to be the receiver for the shadow (in this case the Grid is the ParentBackgroundGrid defined on the page). Note that this logic could definitely be improved. It steps up the visual tree from the current item (i.e. the Grid in the ItemTemplate) all the way up to the Grid defined on the page called RootGrid.

private void RootLoaded(object sender, RoutedEventArgs e)
{
    var parent = VisualTreeHelper.GetParent(this);
    while (parent != null)
    {
        if(parent is Grid grid)
        {
            if(grid.Name == "RootGrid")
            {
                var bg = grid.Children.FirstOrDefault(x => x is Grid g && g.Name == "ParentBackgroundGrid");
                SharedShadow.Receivers.Add(bg);
                return;
            }
        }
        parent = VisualTreeHelper.GetParent(parent);

    }
}

Again, remembering that we can’t specify a ancestor as a Receiver for the shadow. Instead we’re going to find the first child element with the name ParentBackgroundGrid. This Grid will then be set as a Receiver for the ThemeShadow.

As we want to be able to adjust the Z axis translate when the user presses on the item in the list, we need to expose a mechanism whereby the ItemContainerStyle can simply set a property in the visual state definition. We need to define two dependency properties: ContentPadding, which determines the inset of content, and Elevation, which will determine the z translate value.

public Thickness ContentPadding
{
    get { return (Thickness)GetValue(ContentPaddingProperty); }
    set { SetValue(ContentPaddingProperty, value); }
}

public static readonly DependencyProperty ContentPaddingProperty =
    DependencyProperty.Register("ContentPadding", typeof(Thickness), typeof(ItemBackground), new PropertyMetadata(new Thickness(0)));

public int Elevation
{
    get { return (int)GetValue(ElevationProperty); }
    set { SetValue(ElevationProperty, value); }
}

public static readonly DependencyProperty ElevationProperty =
    DependencyProperty.Register("Elevation", typeof(int), typeof(ItemBackground), new PropertyMetadata(0, ElevationChanged));

private static void ElevationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    (d as ItemBackground).ChangeElevation((int)e.NewValue);
}

private void ChangeElevation(int newValue)
{
    Rectangle2.Translation = new Vector3(0, 0, newValue);
}

Let’s give this a run – the following image shows the list of items with the shadow appearing for each item. The only difference between the left and right sides is that in the list on the left side, the item “1000 items – 3” has been pressed.

What’s interesting about applying the shadow to the list of items is that the items almost appear to have rounded corners, in addition to the apparent elevation from the background.

The code in this blog post is very raw and I’ve no doubt there is a way to abstract the various steps to make it easier to define and connect the ThemeShadow with the shadow receiver.

Shadows in Windows (UWP) XAML Applications – Part 1 – ThemeShadow

I’ve been watching James Montemagno bring his AnimalCrossing app to life. The progress continues: #AnimalCrossing Turnip Tracking App powered by #Xamarin and amazing library creators. 99.9% shared code 🙂 pic.twitter.com/7CSlmk6nCJ — James Montemagno – 🔴 Live on Twitch (@JamesMontemagno) April 11, 2020 What’s super impressive is that this doesn’t look anything like a regular out … Continue reading “Shadows in Windows (UWP) XAML Applications – Part 1 – ThemeShadow”

I’ve been watching James Montemagno bring his AnimalCrossing app to life.

What’s super impressive is that this doesn’t look anything like a regular out of the box Xamarin.Forms application. The use of custom fonts, curves, rounded corners and shadows gives this app its own unique look. Unfortunately, it would appear that some of the libraries used aren’t supported on Windows (UWP).

This got me thinking about the use of shadows in UWP, which lead to me sinking time into investigating what is and isn’t support. In this series of posts I’ll walk through some of the options.

Built-in Shadows

Before we get into adding our own shadows to elements in our application, it’s worth noting that UWP already comes with a lot of built-in shadow support. The guidance with regards to using shadows in UWP is that “shadows should be used in a purposeful rather than aesthetic manner”. As such, the built-in shadows appear on elements such as popups and tooltips but don’t appear gratuitously on every list item. For example, the following tooltip has a shadow that makes it appear elevated in relation to the rest of the content.

Dark Mode

I also wanted to see what support there was for dark theme. After switching my Windows theme to dark mode it was evident that there isn’t really any support for shadows. For example, here’s the same tooltip in dark mode.

Whilst there is still some illusion of elevation, this predominately comes from the border that’s been added to the tooltip, rather than an actual shadow. There is definitely no visible shadow around the sides and top of the tooltip, like there is in the light theme.

Now you might be saying “well duh, there is a shadow, you just can’t see it on the black background”. To which, my response is that a shadow, when it comes to applications, is much less about a shadow cast by a light hitting and object, and much more about the illusion of elevation and vertical separation of elements (as described by the Microsoft documentation on Z-depth). As such, even in dark mode you’d expect to see some shadow that illustrates a vertical separation of elements.

WinUI 3

Before we move on, I thought I’d take a quick look at whether there are any difference with regards to shadows in WinUI 3 (grab the WinUI 3 templates to get started). The following image shows the exact same button and tooltip combination, this time using the controls from WinUI 3.

Note that other than a change to the shape and border on the tooltip, there’s really no difference to the shadow cast by the tooltip. The behaviour in dark mode it basically the same too. Although I must admit, do like the new look tooltip with the slightly rounded corners and more gradient border, more than the standard UWP look.

Shadows using ThemeShadow

Whilst support for shadows has been around for quite a while for UWP applications, it was only recent that the ThemeShadow class was added (Windows 10, version 1903 – v10.0.18362.0), making it easier for developers to add shadow support to their applications. To demonstrate this in action, we’ll use a rather contrived example of two overlapping Rectangles, as defined in the following XAML.

<Grid>
    <Rectangle x:Name="Rectangle1"
                Height="200"
                Width="200"
                Fill="Turquoise"
                VerticalAlignment="Top"
                HorizontalAlignment="Right" />
    <Rectangle x:Name="Rectangle2"
                Height="200"
                Width="200"
                Fill="Turquoise"
                VerticalAlignment="Bottom"
                HorizontalAlignment="Left" />
</Grid>

Running this example we can see that both Rectangle shapes appear as one connected shape, flat against the white background. We’ll use a ThemeShadow to elevate the Rectangle2 (bottom left) off the background, and provide some separation from Rectangle1 (top right).

We’ll start by creating a ThemeShadow instance as a Resource with the name SharedShadow. This will be set as the Shadow property on Rectangle2.

<Grid>
    <Grid.Resources>
        <ThemeShadow x:Name="SharedShadow" />
    </Grid.Resources>
    <Grid x:Name="BackgroundGrid" />
    <Rectangle x:Name="Rectangle1"
                Height="200"
                Width="200"
                Fill="Turquoise"
                VerticalAlignment="Top"
                HorizontalAlignment="Right" />
    <Rectangle x:Name="Rectangle2"
                Height="200"
                Width="200"
                Fill="Turquoise"
                VerticalAlignment="Bottom"
                HorizontalAlignment="Left" 
                Shadow="{StaticResource SharedShadow}" />
</Grid>

Note that creating this resource and setting the Shadow property, creates the shadow but it won’t display until you tell the application what elements should show the shadow. This is done by adding elements to the Receivers collection on the ThemeShadow resource.

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    SharedShadow.Receivers.Add(BackgroundGrid);
}

It’s important to note here that you can’t add an ancestor of the element casting the shadow to the Receivers collection. This seems rather counter-intuitive since you’d quite often want the shadow to appear on the background behind the element casting the shadow (which would typically be the parent grid or panel). However, it’s also easy to solve – in the above XAML a Grid called BackgroundGrid was added on which the shadow can be cast.

Even after doing all this, if you run this example you still won’t see a shadow. This is because all the elements are all technically at the same elevation (i.e. they have the same Z axis value of 0) so there is no shadow to be cast. To fix this, we simply need to translate Rectangle2 in the Z dimension.

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    SharedShadow.Receivers.Add(BackgroundGrid);	
    Rectangle2.Translation += new Vector3(0, 0, 32);
}

Now we’re getting somewhere. The following image shows a shadow around Rectangle2. However, it still shows the two rectangles as being a single shape.

The reason there is no shadow being shown on Rectangle1 is that we haven’t told the application that it should show the shadow there. As before, the fix for this is to add Rectangle1 to the Receivers list of the ThemeShadow resource.

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    SharedShadow.Receivers.Add(BackgroundGrid);	
    SharedShadow.Receivers.Add(Rectangle1);
    Rectangle2.Translation += new Vector3(0, 0, 32);
}

And there we have it – a shadow appearing on all sides of Rectangle2.

Of course, we couldn’t just leave it there as I’m sure you’re all wondering what happens in dark mode. Well, there’s good and bad news, as you can see in the following image.

The good news is that the shadow is still being created (see between the rectangles). The bad news is that the shadow hasn’t been inverted with the theme, meaning that you can’t see the shadow against the black background. This is seriously frustrating as it makes the ThemeShadow kind of pointless – it’s main premise was that it was suppose to handle everything for the developer.

Rounded Corners

Updated: I’d forgotten to add this last section on rounded corners in the initial post.

Going back to the designs that James had in the app he’s working on, the items all have rounded corners, giving it a softer, more casual look and feel. To do this in my simple example is relatively easy as I can just set the RadiusX and RadiusY properties.

<Rectangle x:Name="Rectangle2"
            Margin="50"
            Height="200"
            Width="200"
            Fill="Turquoise"
            VerticalAlignment="Bottom"
            HorizontalAlignment="Left"
            RadiusX="40"
            RadiusY="40"
            Shadow="{StaticResource SharedShadow}" />

But what happens to my shadow? Well, the answer is nothing good, as you can see from the following image

In addition to not being able to handle dark mode, it also appears that the ThemeShadow can’t handle non-square corners. According to the documentation, there’s also no support color. Overall the ThemeShadow is severely limited in the scenarios that it would be useful for.

Next up we’ll take a more in depth look at the Shadow property in UWP and how you can implement a shadow yourself.

Dual Screens with Surface Neo (Windows 10X) and Surface Duo (Android)

In October 2019 Microsoft announced a new category of dual screen device with flagship products Surface Neo and Surface Duo. Since then, there has been a massive undertaking by Microsoft to prepare developers of both Windows (UWP and Win32 apps) and Android apps for a new world of dual, or even multiple, screen devices. This … Continue reading “Dual Screens with Surface Neo (Windows 10X) and Surface Duo (Android)”

In October 2019 Microsoft announced a new category of dual screen device with flagship products Surface Neo and Surface Duo. Since then, there has been a massive undertaking by Microsoft to prepare developers of both Windows (UWP and Win32 apps) and Android apps for a new world of dual, or even multiple, screen devices. This included a day focused on building dual screen applications – Microsoft 365 Developer Day: Dual Screens (and here) (this event in itself was weird because it was branded as Microsoft 365 but other than a session on Microsof Graph it had very little to do with Microsoft 365 and everything to do with Windows and Android SDKs for dual screen devices). In this post I wanted to start exploring the dual screen emulators and talk about some points of interest when it comes to interacting with the screens.

Firstly, if you haven’t already, you can grab the emulator images either from the Microsoft Store (for the Windows 10X emulator) or via the Surface Duo SDK. Start at the documentation page for how to Get the Windows 10X development tools and how to Get the Surface Duo SDK.

Now that you’ve got the tools, let’s start to take a look at the emulators. Specifically I want to drill into how the different platforms handle spanning an application across the fold/hinge. The following image shows an image being spanned across the hinge on the Windows 10X emulator.

Notice how there’s a break in the image, making the image feel like it’s broken in two. What’s interesting is that when you take a screenshot using the emulator tools, this break isn’t visible, as shown in the following screenshot.

If we us the Maps application and again span the hinge, there is again a break across the hinge, making the map hard to process visually.

If we switch across to the Android emulator, you can now see that the image spans across the hinge without disturbing the image. The hinge masks or obscures part of the picture, but this is much easier for us to deal with.

And again, using the Android emulator you can see that the map continues beneath the hinge.

Ok, so let’s jump into taking a look at some code. In this case we’re going to look at some UWP code but rather than attempting to treat the two screens as a single page (as shown in soooooo many demos/samples) we’re going to use multiple windows for the application. Into a new UWP application I’ve added a Button and a TextBlock (just to show which window is which). The Button is used to launch new Windows, each showing the MainPage.

private async void Button_Click(object sender, RoutedEventArgs e)
{
    CoreApplicationView newView = CoreApplication.CreateNewView();
    int newViewId = 0;
    await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
        Frame frame = new Frame();
        frame.Navigate(typeof(MainPage), null);
        Window.Current.Content = frame;
        // You have to activate the window in order to show it later.
        Window.Current.Activate();

        newViewId = ApplicationView.GetForCurrentView().Id;
    });
    bool viewShown = await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId);
}

When I first run this, I can see my application taking up a single screen.

Clicking the button the second Window appears on the other screen – this is exactly what you’d hope would happen.

If we now press the button on the second Window, another new Window appears, again on the alternative screen.

Now we can start to arrange the Windows. Here I’ve dragged Window 3 into the lower right corner of the second screen, below Window 2. (ignoring the mouse cursor, which was a result of me attempting to screen capture the drag experience) You can see that there is a visual cue as to what will happen when I release the drag operation.

Window 3 expands to take the lower half of the second screen and prompts me to select what I want to take the top half.

Clicking on Window 2 it expands to fill the top half of the screen.

What’s interesting about this example, which uses Multiple Windows instead of dual panes or a single Window that spans the hinge, is that you get complete control over how the individual windows are positioned. The default behaviour works nicely with Windows appearing in the correct screen, yet you can easily override it by dragging the windows to where you want them.

Along the way (probably because so many apps are modeled on the iOS notion of only doing one thing at a time) we as app developers have forgotten how to take advantage of multiple Windows. Now is the time to think about how best to extend your application to use multiple windows and prepare it for the next wave of multi-screen devices.

Look iOS Developer, No Mac Required – Build an iOS Application using Xamarin and Visual Studio for Windows without using a Mac

If you’re a die-hard Windows user, like me, you’ll be excited to know that you can now build iOS application using Xamarin (and Xamarin.Forms) and Visual Studio, without having to buy or use a Mac. That’s right for development, you no longer need to invest, or carry around, a Mac. In this post I’ll walk you through how to enable this feature.

If you’re a die-hard Windows user, like me, you’ll be excited to know that you can now build iOS application using Xamarin (and Xamarin.Forms) and Visual Studio, without having to buy or use a Mac. That’s right for development, you no longer need to invest, or carry around, a Mac. In this post I’ll walk you through how to enable this feature.

I’m working in the latest preview of Visual Studio 2019 and as of now, to use Xamarin Hot Restart (the feature that powers the no Mac development experience), you need to check the Enable Xamarin Hot Restart option under Preview Features in the Tools, Options dialog.

After enabling Xamarin Hot Restart, make sure you restart Visual Studio. Next, set your iOS project to be the startup project by right-clicking the iOS project in Solution Explorer, followed by the Set as Startup Project menu item.

Make sure in the toolbar the option next to the play button says Local Device. Click the play button, which will kick off the Setup Hot Restart experience.

There are a couple of steps to jump through the first time you want to use Hot Restart. Make sure you follow the instructions to avoid having to redo steps.

The first step in the process it to Download iTunes – make sure you click the Download iTunes button. Do NOT install iTunes from the Microsoft Store. If you’ve done this previously, make sure you uninstall it, and then install it by clicking the Download iTunes button.

Clicking the Download iTunes button will open your default browser but it will attempt to immediately download the file – make sure you check out the downloads so you can launch the file once it’s downloaded.

Step through the iTunes installer.

Once iTunes is installed the Setup Hot Restart process will detect the presence of iTunes (note that it will not detect iTunes if you’re installed it from the Microsoft Store).

Next, make sure you have an iOS device plugged in and that you’ve clicked the Trust option on the device when prompted to trust the connected computer. The Setup Hot Restart process should detect the attached device.

Next, sign into your Apple Developer account.

And select the Development Team you want to use for provisioning.

After completing the Setup Hot Restart process you should see that the build process will continue and that in the Output window you’ll see the iPA being created and subsequently pushed to the device.

When prompted, you’ll need to launch the installed application on the iOS device – this manual step is required in order for Visual Studio to attach the debugger.

And there you have it – you now have an iOS application being debugged using Visual Studio on an actual device with no Mac required!!!

Pipeline Templates: Complete Azure Pipelines Example for a Uno Project for iOS, Android and Windows

My last post was a bit of a long one as it covered a bunch of steps for setting up the bits and pieces required for signing an application for different platforms. In this post I just wanted to provide a complete example that shows a single multi-stage (6 in total) Azure Pipelines pipeline for … Continue reading “Pipeline Templates: Complete Azure Pipelines Example for a Uno Project for iOS, Android and Windows”

My last post was a bit of a long one as it covered a bunch of steps for setting up the bits and pieces required for signing an application for different platforms. In this post I just wanted to provide a complete example that shows a single multi-stage (6 in total) Azure Pipelines pipeline for building a Uno application for iOS, Android and Windows (UWP) and releasing them to App Center.

Secure Files

In my last post I showed how to create and populate Secure Files in Azure Pipelines. Any certificate or provisioning profile you need to use in your pipeline should be added to the Secure Files section of the Library in Azure Pipeline. My list of Secure Files looks like this:

Here we can see that we have the signing certificates for iOS and Windows, and the keystore for Android. Then we have two iOS provisioning profiles, one for my XF application and the other for my Uno application.

Variable Groups

I’ve extracted most of the variables I use in my pipeline into one of two variable groups:

The Common Build Variables are those variables that can be reused across multiple projects.

The Inspector Uno Build Variables are those variables that are specific to this project. For example it includes the AppCenter ids for the iOS, Android and Windows applications. It also includes the iOS provisioning profile which is specifically tied to this application.

Pipeline

Here’s the entire pipeline:

resources:
  repositories:
    - repository: builttoroam_templates
      type: github
      name: builttoroam/pipeline_templates
      ref: refs/tags/v0.5.0
      endpoint: github_connection
  
variables:
  - group: 'Common Build Variables'
  - group: 'Inspector Uno Build Variables'
  - name: ios_enabled
    value: 'true'
  - name: windows_enabled
    value: 'true'
  - name: android_enabled
    value: 'true'
 
stages:
- template:  azure/mobile/[email protected]_templates
  parameters:
    # Stage name and whether it's enabled
    stage_name: 'Build_Android'
    build_android_enabled: $(android_enabled)
    # Version information
    full_version_number: '$(version_prefix).$(Build.BuildId)'
    # Signing information
    secure_file_keystore_filename: '$(android_keystore_filename)'
    keystore_alias: '$(android_keystore_alias)'
    keystore_password: '$(android_keystore_password)'
    # Solution to build
    solution_filename: $(solution_file)
    solution_build_configuration: $(solution_build_config)
    # Output information
    artifact_folder: $(artifact_android_folder)
    application_package: $(android_application_package)

- template:  azure/mobile/[email protected]_templates
  parameters:
    # Stage name and dependencies
    stage_name: 'Deploy_Android'
    depends_on: 'Build_Android'
    deploy_appcenter_enabled: $(android_enabled)
    environment_name: $(appcenter_environment)
    # Build artifacts
    artifact_folder: $(artifact_android_folder)
    application_package: $(android_application_package)
    # Signing information (for Android repack to APK)
    secure_file_keystore_filename: '$(android_keystore_filename)'
    keystore_alias: '$(android_keystore_alias)'
    keystore_password: '$(android_keystore_password)'
    # Deployment to AppCenter
    appcenter_service_connection: $(appcenter_service_connection)
    appcenter_organisation: $(appcenter_organisation)
    appcenter_applicationid: $(appcenter_android_appid)


- template:  azure/mobile/[email protected]_templates
  parameters:
    # Stage name and whether it's enabled
    stage_name: 'Build_Windows'
    build_windows_enabled: $(windows_enabled)
    # Version information
    full_version_number: '$(version_prefix).$(Build.BuildId)'
    # Signing information
    windows_cert_securefiles_filename: '$(windows_signing_certificate_securefiles_filename)'
    windows_cert_password: '$(windows_signing_certificate_password)'
    # Solution to build
    solution_filename: $(solution_file)
    solution_build_configuration: $(solution_build_config)
    # Output information
    artifact_folder: $(artifact_windows_folder)
    application_package: $(windows_application_package)

- template:  azure/mobile/[email protected]_templates
  parameters:
    # Stage name and dependencies
    stage_name: 'Deploy_Windows'
    depends_on: 'Build_Windows'
    deploy_appcenter_enabled: $(windows_enabled)
    environment_name: $(appcenter_environment)
    # Build artifacts
    artifact_folder: $(artifact_windows_folder)
    application_package: $(windows_application_package)
    # Deployment to AppCenter
    appcenter_service_connection: $(appcenter_service_connection)
    appcenter_organisation: $(appcenter_organisation)
    appcenter_applicationid: $(appcenter_windows_appid)

- template:  azure/mobile/[email protected]_templates
  parameters:
    # Stage name and whether it's enabled
    stage_name: 'Build_iOS' 
    build_ios_enabled: $(ios_enabled)
    # Version information
    full_version_number: '$(version_prefix).$(Build.BuildId)'
    # Solution to build
    solution_filename: $(solution_file)
    solution_build_configuration: $(solution_build_config)
    # Signing information
    ios_plist_filename: 'src/Apps/DotNet/Uno/InspectorUno/InspectorUno/InspectorUno.iOS/Info.plist'
    ios_cert_password: '$(ios_signing_certificate_password)'
    ios_cert_securefiles_filename: '$(ios_signing_certificate_securefiles_filename)'
    ios_provisioning_profile_securefiles_filename: '$(ios_provisioning_profile_securefiles_filename)'
    # Output information
    artifact_folder: $(artifact_ios_folder)
    application_package: $(ios_application_package)

- template:  azure/mobile/[email protected]_templates
  parameters:
    # Stage name and dependencies
    stage_name: 'Deploy_iOS'
    depends_on: 'Build_iOS'
    deploy_appcenter_enabled: $(ios_enabled)
    environment_name: $(appcenter_environment)
    # Build artifacts
    artifact_folder: $(artifact_ios_folder)
    application_package: $(ios_application_package)
    # Deployment to AppCenter
    appcenter_service_connection: $(appcenter_service_connection)
    appcenter_organisation: $(appcenter_organisation)
    appcenter_applicationid: $(appcenter_ios_appid)


Pipeline Templates v0.5.0

Most of the v0.5.0 release has been tidying things up, reducing the number of required parameters by making the template smarter and increasing consistency across the templates

Breaking Changes:

  • build-xamarin-android.yml – changed build_platform to solution_target_platform parameter
  • build-xamarin-windows.yml – changed build_platform to solution_target_platform parameter
  • build-xamarin-windows.yml – changed windows_appxupload_name parameter to windows_upload_name to reflect support for msix

Other Changes:

  • build-xamarin-[iOS/android/windows].yml – added depends_on parameter so that the stages can be ordered
  • build-xamarin-[iOS/android/windows].yml – artifact_name, artifact_folder and application_package are no longer required and have default values
  • build-xamarin-android.yml – supports building either aab or apk based on the application_package parameter
  • build-xamarin-windows.yml – windows_upload_name parameter isn’t required as it has a default value based on the bundle name
  • deploy-appcenter.yml – added conditions to all steps so that pipeline doesn’t break if build stage doesn’t generate any output
  • All template – documentation added to parameters and steps

Pipeline Templates: Building and Deploying Uno Apps for iOS, Android and Windows

In my previous posts covering the Pipeline Templates I’ve discussed building a Xamarin.Forms apps for iOS, Android and Windows (UWP) and subsequently deploying them to AppCenter. In this post we’re going to look at doing the same with a Uno application. Given that Uno is built on top of the core Xamarin functionality, the process … Continue reading “Pipeline Templates: Building and Deploying Uno Apps for iOS, Android and Windows”

In my previous posts covering the Pipeline Templates I’ve discussed building a Xamarin.Forms apps for iOS, Android and Windows (UWP) and subsequently deploying them to AppCenter. In this post we’re going to look at doing the same with a Uno application. Given that Uno is built on top of the core Xamarin functionality, the process for both build and deploy should be fairly similar. Rather than just stepping through using the template, I’m going to cover creating a new Uno project and setting up the corresponding multi-stage pipeline to build and deploy to AppCenter.

Also, I’ve pushed v0.3.0 of the pipeline templates, which I’ll summarise at the end of this post.

Creating a Uno Application

Let’s get into it and start by creating a Uno application. Before proceeding, make sure you grab the latest Uno extension for Visual Studio that includes the project templates. From the Create a new project dialog, enter “uno” into the search box and then select the Cross-Platform App (Uno Platform) template.

Next, enter a name and location for your new application

When you hit the Create button, Visual Studio will generate a new solution with five projects, representing the head projects for iOS, Android, Windows (UWP) and Web (WASM), along with a shared project.

After creating the application, you should go through each platform and check that it builds and runs. The WASM project will take a while to build and run the first time as it needs to download some components first – don’t be alarmed it nothing appears to happen for a long time.

Build Configuration

One thing I do like to tidy up for my applications is the build configurations. This is often overlooked by developers and then wonder why they have to wait around for say the Android project to build, when they have the UWP project selected as their start up project. Here are the Release configurations I have set for the different platforms

Release Configuration with Any CPU
Release Configuration with iPhone
Release Configuration with x64 (x86 and ARM platforms are the same except the Platform for InspectorUno.UWP is x86 and ARM respectively)

Building Uno Applications

We’ll go through each Uno application separately and I’ll endeavor to highlight typical pain points that developers face. If you’re using the pipeline_templates and you run into issues, message me on Twitter and I’ll help you out. Where there are common frustrations, I’ll work to improve the templates to make them easier to use.

Let’s start by creating a new pipeline in Azure DevOps. I’m not going to step through the pipeline wizard but for what we want to do, just select the appropriate source code respository and an empty YAML file. When you get to the YAML editor, enter the following.

trigger: none
resources:
  repositories:
    - repository: builttoroam_templates
      type: github
      name: builttoroam/pipeline_templates
      ref: refs/tags/v0.3.0
      endpoint: github_connection
  
variables:
  - group: 'Uno Build Variables'

There are three parts to this inital YAML. Firstly, we’re disabling the CI trigger whilst we’re configuring the build. As we’ll be committing changes to both the build pipeline and source code, it’s as well to only have builds triggered when you’re ready, otherwise you’ll be continually cancelling builds.

Next, the resources section is where we pull in the pipeline templates. In this case we’re targeting the v0.3.0 release to ensure the templates don’t change and break our builds in the future.

Lastly, we’re pulling in the Uno Build Variables variable group. You can pick whatever name you want for this group but it needs to match the name of the variable group defined under the Library tab on Azure DevOps.

Android – Build

Starting with Android we’re going to add stages to the YAML file (note you only need to include the stages element once and then add the individual stages by referencing the corresponding template). Here’s the Android build

stages:
- template:  azure/mobile/[email protected]_templates
  parameters:
    stage_name: 'Build_Android'
    build_android: $(android_enabled)
    solution_filename: 'src/Apps/DotNet/Inspector.Uno.sln'
    solution_build_configuration: 'Release'
    build_number: '$(Build.BuildId)'
    full_version_number: '$(version_prefix).$(Build.BuildId)'
    artifact_name: 'inspector-build'
    artifact_folder: 'Android_output'
    application_package: 'Inspector-Uno-Android.aab'
    secure_file_keystore_filename: '$(android_keystore_filename)'
    keystore_alias: '$(android_keystore_alias)'
    keystore_password: '$(android_keystore_password)'

There are a few things you need to have setup for this build to work:

  • solution_filename – make sure the path to your solution file is correct, relevant to the root of your code repository.
  • $(version_prefix) – make sure the version_prefix variable exists in your variable group and has the format X.Y (eg 1.0)
  • $(android_keystore_filename) – make sure the android_keystore_filename variable exists in your variable group and that it’s value matches the Secure file name of the keystore file uploaded to the Secure files under the Library in Azure DevOps.
  • $(android_keystore_alias) – make sure the android_keystore_alias variable exists in your variable group and that it is the the alias that you specified when creating the keystore. This variable should be marked as private by clicking the lock beside it in the portal – this hides it both in the variable group editor in the portal and in the log files.
  • $(android_keystore_password) – make sure the android_keystore_password variable exists in your variable group and that it is the password you specified when creating the keystore. Again this should be marked as private.

Frustratingly, the Android build didn’t just work for a couple of reasons:

  • Firstly, there was an issue where one of the images has a file name that isn’t supported by the version of Visual Studio and/or Android tooling on the windows-latest build agent, resulting in the following error:
    2020-02-08T06:04:55.7036355Z ##[error]src\Apps\DotNet\Uno\InspectorUno\InspectorUno\InspectorUno.Shared\Assets\Square44x44Logo.targetsize-24_altform-unplated.png(0,0): Error APT0003: Invalid file name: It must contain only [^a-zA-Z0-9_.]+.
    I simply excluded the file Square44x44Logo.targetsize-24_altform-unplated.png from the shared project
  • Secondly, the build agent include the Android NDK but doesn’t specify the appropriate environment variables (eg ANDROID_NDK_HOME), resulting in the following error:
    2020-02-07T12:56:19.5428557Z ##[error]C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Xamarin\Android\Xamarin.Android.Common.targets(2843,3): Error XA5101: Could not locate the Android NDK. Please make sure the Android NDK is installed in the Android SDK Manager, or if using a custom NDK path, please ensure the $(AndroidNdkDirectory) MSBuild property is set to the custom path.
    The solution here was for me to update the build-xamarin-android.yml to include these environment variables. You shouldn’t run into this error if you’re using v0.3.0 of the pipeline templates.

Android – Deploy

Here’s the YAML for deploying the Android application to AppCenter

- template:  azure/mobile/[email protected]_templates
  parameters:
    stage_name: 'Deploy_Android'
    depends_on: 'Build_Android'
    environment_name: 'Inspector-Alpha'
    artifact_name: 'inspector-build'
    artifact_folder: 'Android_output'
    application_package: 'Inspector-Uno-Android.aab'
    appcenter_service_connection: 'AppCenterInspectorCI'
    appcenter_organisation: 'thenickrandolph'
    appcenter_applicationid: 'Inspector-Uno-Alpha'
    appcenter_release_notes: 'Release from deploy pipeline'
    secure_file_keystore_filename: '$(android_keystore_filename)'
    keystore_alias: '$(android_keystore_alias)'
    keystore_password: '$(android_keystore_password)'

Again there are a few things you need to check here but most importantly for this to work you need to setup an application in AppCenter. I created a new application

After creating the application, look at the url that appears in the browser. The piece highlighted in green is the value you need to set for the appcenter_organisation and the yellow is for the appcenter_applicationid.

You also need to make sure you setup a Service Connection between Azure DevOps and AppCenter (in this case it was given the name ‘AppCenterInspectorCI’ and assigned to the appcenter_service_connection property)

The other properties you need to check are:

  • depends_on – make sure this matches the stage_name property set on the android build stage
  • artifact_name, artifact_folder and application_package – make sure these match the corresponding value on the android build stage
  • secure_file_keystore_filename, keystore_alias and keystore_password – these should be set to the same as on the android build stage and are required so that the release process can extract and sign a fat apk from the aab file.

iOS – Build

Before we get into building your iOS application, let’s make sure you have both a signing certificate and a provisioning profile ready to go. If you have both of these, you can jump over the next two sections. These instructions can be stepped through on Windows, without the need for a Mac!

certificate

  • Install OpenSSL (Download from https://slproweb.com/products/Win32OpenSSL.html)
  • Open command prompt
  • Add OpenSSL to path (alternatively you can permanently add it to your Path environment variable if you want to have it accessible across all command prompts)
    set PATH=%PATH%;C:\Program Files\OpenSSL-Win64\bin\
  • Generate the signing request
    openssl genrsa -out ios_distribution.key 2048
    openssl req -new -key ios_distribution.key -out CertificateSigningRequest.certSigningRequest
  • Sign into the Apple Developer portal and go to Certificates (https://developer.apple.com/account/resources/certificates)
  • Click the + button to start the process of adding a new certificate
  • Select Apple Distribution from the list of certificate types and then Continue
  • Use the Choose File link to select the CertificateSigningRequest.certSigningRequest file you created in the previous steps, and then Continue
  • Important – Make sure you click the Download button to download your file. It’ll download as a file called distribution.cer unless you choose to rename it.
  • Move the distribution.cer file to the same folder that you’ve been working in
  • Complete the certificate creation from the command prompt (making sure you change PASSWORD to be a password of your choice)
    openssl x509 -in distribution.cer -inform DER -out ios_distribution.pem -outform PEM
    openssl pkcs12 -export -inkey ios_distribution.key -in ios_distribution.pem -out ios_distribution.p12 -passout pass:PASSWORD

With these steps completed you have a certificate file, ios_distribution.p12, which you can upload as a Secure File to Azure DevOps.

You’ll also need the signing identity, which we can get directly from the ios_distribution.pem that was generated along the way.
openssl x509 -text -noout -in ios_distribution.pem

Application Identifier

Before you can create a provisioning profile you need to setup an identity for your application in the Apple Developer portal. Go to the Identities tab and click on the + button to register a new application.

Select App IDs and then Continue

Enter both a Description and a Bundle ID. You may also need to select an App ID Prefix if you belong to a team. Next click Continue, followed by the Register button to complete the process of registering your application’s identity.

Note: Currently you will need to update the Info.plist file of your iOS application to set the CFBundleIdentifier to match the Bundle ID specified when creating the App ID.

Provisioning Profile

To create the provisioning profile, click on the Profiles tab in the Apple Developer portal. Click the + button to start the process of creating a new provisioning profile.

Select the Ad Hoc option under Distribution and then Continue.

Select the App ID that you previously created, then Continue

Select the certificate that needs to be used to sign the application, then Continue

Select the devices that will be able to install the application, then Continue

Give the provisioning profile a name and then click Generate.

Download the provisioning profile and add it as a Secure File in the Library in Azure DevOps. You’ll also need the provisioning profile id, which you can extract from the provisioning profile using any text editor. Open the provisioning profile and look for the UUID key; the string value immediately after it is the profile id.

Ok, we’re now ready for the build stage

- template:  azure/mobile/[email protected]_templates
  parameters:
    stage_name: 'Build_iOS' 
    build_ios: true
    solution_filename: 'src/Apps/DotNet/Inspector.Uno.sln'
    solution_build_configuration: 'Release'
    ios_cert_password: '$(ios_signing_certificate_password)'
    ios_cert_securefiles_filename: '$(ios_signing_certificate_securefiles_filename)'
    ios_provisioning_profile_securefiles_filename: '$(ios_provisioning_profile_securefiles_filename)'
    build_number: '$(Build.BuildId)'
    full_version_number: '$(version_prefix).$(Build.BuildId)'
    ios_signing_identity: '$(ios_signing_identity)'
    ios_provisioning_profile_id: '$(ios_provisioning_profile_id)'
    artifact_name: 'inspector-build'
    artifact_folder: 'iOS_output'
    application_package: 'Inspector-Uno-iOS.ipa'

iOS – Deploy

The deploy stage is relatively simple

- template:  azure/mobile/[email protected]_templates
  parameters:
    stage_name: 'Deploy_iOS'
    depends_on: 'Build_iOS'
    environment_name: 'Inspector-Alpha'
    artifact_name: 'inspector-build'
    artifact_folder: 'iOS_output'
    application_package: 'Inspector-Uno-iOS.ipa'
    appcenter_service_connection: 'AppCenterInspectorCI'
    appcenter_organisation: 'thenickrandolph'
    appcenter_applicationid: 'Inspector-Uno-iOS-Alpha'
    appcenter_release_notes: 'Release from deploy pipeline'

Windows – Build

The build stage for Windows (UWP) is

- template:  azure/mobile/[email protected]_templates
  parameters:
    stage_name: 'Build_Windows'
    build_windows: true
    solution_filename: 'src/Apps/DotNet/Inspector.Uno.sln'
    solution_build_configuration: 'Release'
    uwpBuildPlatform: 'x86'
    build_number: '$(Build.BuildId)'
    full_version_number: '$(version_prefix).$(Build.BuildId)'
    windows_cert_securefiles_filename: '$(windows_signing_certificate_securefiles_filename)'
    windows_cert_password: '$(windows_signing_certificate_password)'
    artifact_name: 'inspector-build'
    artifact_folder: 'Windows_output'
    application_package: 'Inspector-Uno-Windows.msixbundle'
    windows_appxupload_name: 'Inspector-Uno-Windows.msixupload'

In order to build your Windows application you’re going to need a signing certificate. Microsoft has good documentation for creating the certificate which I would encourage you to follow. The important thing is to upload the certificate to the Secure files section under the Library tab on Azure DevOps and the corresponding password as a private variable. Damien Aicheh has a good tutorial on uploading certificates to Azure DevOps – don’t worry about the section on using the certificates as the Windows build template already does this for you.

Note: You also don’t need to worry about updating your package.appxmanifest so that the Publisher matches the Subject on the signing certificate. The Windows build template takes care of this too.

There are a couple of other parameters you’ll want to check off

  • uwpBuildPlatform – in this YAML this is only set to x86 for efficiency. If you’re planning to deploy your application to a range of devices, you should set this to x86|x64|ARM (this is the default value on the template so you can omit this property if you want to build for all three platforms)
  • $(windows_signing_certificate_securefiles_filename) – make sure the windows_signing_certificate_securefiles_filename variable exists in your variable group and that it’s value matches the Secure file name of the certificate (.pfx) file uploaded to the Secure files under the Library in Azure DevOps.
  • $(windows_signing_certificate_password) – make sure the windows_signing_certificate_password variable exists in your variable group and that it is password you specified when creating the certificate. This should be marked as private.

Note that the application package that we’re specifying here is an msixbundle. The Windows build pipeline has been upgraded to support both appx and msix bundles.

Windows – Deploy

The deploy stage for Windows (UWP) is

- template:  azure/mobile/[email protected]_templates
  parameters:
    stage_name: 'Deploy_Windows'
    depends_on: 'Build_Windows'
    environment_name: 'Inspector-Alpha'
    artifact_name: 'inspector-build'
    artifact_folder: 'Windows_output'
    application_package: 'Inspector-Uno-Windows.msixbundle'
    appcenter_service_connection: 'AppCenterInspectorCI'
    appcenter_organisation: 'thenickrandolph'
    appcenter_applicationid: 'Inspector-Uno-UWP-Alpha'
    appcenter_release_notes: 'Release from deploy pipeline'

Pipeline Templates v0.3.0

As promised, here’s a quick summary of the changes in v0.3.0. Technically this should have been a major version increment as there were some breaking changes but since we’re not at v1 yet, I’ve just pushed a minor update.

Breaking Changes:

  • build-xamarin-android.yml – change android_bundle_name parameter to application_package (for consistency with deploy-appcenter.yml)
  • build-xamarin-ios.yml – change ios_ipa_name parameter to application_package (for consistency with deploy-appcenter.yml)
  • build-xamarin-windows.yml – change windows_bundle_name parameter to application_package (for consistency with deploy-appcenter.yml)

Other Changes:

  • build-xamarin-android.yml – added environment variables (ANDROID_NDK_HOME, ANDROID_NDK_PATH, AndroidNdkDirectory) so that the build process can locate the NDK. This is required for Android builds that enable AOT compilation.
  • build-xamarin-android.yml: android_manifest_filename property no longer required – template will search for AndroidManifest.xml
  • build-xamarin-windows.yml: windows_package_manifest_filename property no longer required – template will search for *.appxmanifest
  • build-xamarin-ios.yml: ios_plist_filename property no longer required – template will search for Info.plist
  • build-xamarin-windows.yml: application_package supports both appxbundle and msixbundle file types. You need to make sure the filename matches the type of bundle your application is setup to create.

Deploy Xamarin.Forms Apps to App Center from a Azure Multi-Stage Pipeline using Templates and Environments that Require Manual Approval

Wow, that title’s a mouthful, and I didn’t add in there that I’ve just pushed v0.2.0 release of the Pipeline Templates repository. In this post we’re going to add stages to a YAML based Azure DevOps pipeline in order to deploy a Xamarin.Forms application to AppCenter for testing. We’ll also be using on the of … Continue reading “Deploy Xamarin.Forms Apps to App Center from a Azure Multi-Stage Pipeline using Templates and Environments that Require Manual Approval”

Wow, that title’s a mouthful, and I didn’t add in there that I’ve just pushed v0.2.0 release of the Pipeline Templates repository. In this post we’re going to add stages to a YAML based Azure DevOps pipeline in order to deploy a Xamarin.Forms application to AppCenter for testing. We’ll also be using on the of the latest features of the Azure DevOps YAML based pipelines, Environments, to insert a manual approval gate into our multi-stage pipeline.

Pipeline Templates v0.2.0

Before we get into talking about releasing apps to AppCenter I just wanted to reiterate that there is a new release of the Pipeline Templates repository with the following changes:

  • Added a new parameter, stage_name, to iOS, Android and Windows build templates. It has a default value, which matches the value previously specified on the stage element in the template, so won’t break any existing builds. This parameter can be set by the calling pipeline so that the stage is given a known name, which can then be referenced by other stages in the dependsOn element.
  • Added deployappcenter.yml template that can be used to deploy iOS, Android and Windows apps to AppCenter. For Android, if the application_package parameter is an .aab file, the calling pipeline will also need to supply keystore information. AppCenter doesn’t support .aab files, so the pipeline uses bundletool to generate and sign a fat apk, which is submitted to AppCenter.

Deploy to AppCenter

With a classic pipelines in Azure DevOps, you can setup separate build and release pipelines. However, YAML pipelines don’t differentiate between build and release pipelines; instead you can split a single pipeline into multiple stages (as we demonstrated in the previous post when we used different templates to create different stages in the build process).

To deploy apps to AppCenter we could simply create a template, similar to what we did with the build templates, that includes the tasks necessary to deploy to AppCenter, and then add new stages to our pipeline for each app we want to deploy. However, this would limit our ability to take advantage of some of the deployment specific features that are available in Azure DevOps. For this reason, the AppCenter template makes use of a deployment job in order to do the steps necessary to release an app to AppCenter.

Using the AppCenter Template

The following YAML pipeline provides an example of using the new AppCenter deployment template to deploy Windows (UWP), Android and iOS applications to AppCenter. Note that the ref element for the repository resource has been updated to point to the v0.2.0 tag.

resources:
  repositories:
    - repository: builttoroam_templates
      type: github
      name: builttoroam/pipeline_templates
      ref: refs/tags/v0.2.0
      endpoint: github_connection
  
variables:
  - group: 'Inspector XF Build Variables'
  
stages:
## Build stages excluded for brevity
- template:  azure/mobile/[email protected]_templates
  parameters:
    stage_name: 'Deploy_Windows'
    depends_on: 'Build_Windows'
    environment_name: 'Inspector-Alpha'
    artifact_name: 'inspector-build'
    artifact_folder: 'Windows_output'
    application_package: 'Inspector-XF-Windows.appxbundle'
    appcenter_service_connection: 'AppCenterInspectorCI'
    appcenter_organisation: 'thenickrandolph'
    appcenter_applicationid: 'Inspector-XF-UWP'
    appcenter_release_notes: 'Release from deploy pipeline'

- template:  azure/mobile/[email protected]_templates
  parameters:
    stage_name: 'Deploy_Android'
    depends_on: 'Build_Android'
    environment_name: 'Inspector-Alpha'
    artifact_name: 'inspector-build'
    artifact_folder: 'Android_output'
    application_package: 'Inspector-XF-Android.aab'
    appcenter_service_connection: 'AppCenterInspectorCI'
    appcenter_organisation: 'thenickrandolph'
    appcenter_applicationid: 'Inspector-XF-Android-Alpha'
    appcenter_release_notes: 'Release from deploy pipeline'
    secure_file_keystore_filename: '$(android_keystore_filename)'
    keystore_alias: '$(android_keystore_alias)'
    keystore_password: '$(android_keystore_password)'

- template:  azure/mobile/[email protected]_templates
  parameters:
    stage_name: 'Deploy_iOS'
    depends_on: 'Build_iOS'
    environment_name: 'Inspector-Alpha'
    artifact_name: 'inspector-build'
    artifact_folder: 'iOS_output'
    application_package: 'Inspector-XF-iOS.ipa'
    appcenter_service_connection: 'AppCenterInspectorCI'
    appcenter_organisation: 'thenickrandolph'
    appcenter_applicationid: 'Inspector-XF-iOS-Alpha'
    appcenter_release_notes: 'Release from deploy pipeline'

There are a couple of prerequisites that need to be setup in order for the deploy stages to work:

  • Each deploy stage specifies a depends_on property. This needs to correlate to the stage_name property specified on the corresponding build stage.
  • The artifact name, artifact folder and application_package properties need to match to the values used in the corresponding build stage.
  • A Service Connection needs to be established between Azure DevOps and AppCenter (in this case it was given the name ‘AppCenterInspectorCI’)
  • An application needs to be registered in AppCenter for each target platform. Each deploy stage needs the organisation (ie username or org_name) and applicationid (app_identifier). See the documentation on the AppCenterDistribute task for more information on how to find these values for your AppCenter apps.

Manual Approval with Environments

I mentioned earlier that using a deployment job would allow us to take advantage of deployment specific features. This is an area that’s currently under development and we’d expect to see more features lighting up in this area over time.

One feature that’s available today is the ability to add manual approval requirement to a deployment job. However, unlike in the classic pipeline where you’d create a manual approval requirement directly on the release pipeline, on a YAML pipeline you actually need to associated the deployment job with an environment and then add a manual approval on the environment.

You may have noticed that each of the deploy stages in the example YAML specified the environment_name property. This defines which environment you’re going to be deploying to. At this stage the only thing you can use this for in terms of deploying a mobile application is to require manual approval for the stage to continue. Let’s step through creating the environment and the approval requirement and you’ll see what I mean.

Create an Azure Pipelines Environment

Under the Pipelines tab, select Environments and then click the Create environment button in the center of the screen.

Next, provide a name for your environment and click Create. At this stage we don’t need to define any resources, so you can leave the default selection of “None”. The name that you specify for your environment has to match what you use as the environment_name property on the template.

Adding Manual Approval to the Environment

In order to add a manual approval requirement to an environment, simply open the environment (if you’ve just created the environment you’re already there). From the drop-down menu in the top-right corner, select Approvals and checks.

Next, click the + button in the top right corner.

Select Approvals, and then click Next

In the Approvals flyout you can specify a list of users and/or groups that need to approve a release to the environment. If you specify multiple users, each user needs to approve the release. If you specify a group, only one person in the group needs to approve the release.

In the Approvals flyout you can also specify a timeout; if the deployment isn’t approved for an environment within the timeout, the pipeline will fail.

Note: Currently there are no emails, or other notifications, sent to approvers. If you limit the timeout, once the specified period has elapsed if the environment hasn’t been approved, the pipeline fail and a notification will be sent out. The stage in the pipeline that failed can then be manually run and again, approval for the environment will be required.

Running the Build and Deploy Pipeline

Now when we run the pipeline, what we see in the portal is three different rows (one for each platform) with two stages (a build and deploy stage).

We’ve set the dependsOn element on each of the build stages to an empty array (ie []) meaning that they have no dependencies (btw the default is that stages will be done in the sequence that they appear in the YAML file, unless you indicate there should be no dependencies). Depending on how many build agents you have at your disposal the build stages may run in sequence, or in parallel.

Eventually, when each of the build stages completes, the corresponding deploy stage will light up indicating that it’s waiting for a check to be passed. There’s also a message box inserted into the interface to draw attention to the required approval.

Once all three of the build stages are complete, there are 3 approvals required; one for each of the deploy stages. The way we’ve structured the pipeline you have to approve the deployment for each platform.

If you wanted to require only a single approval for all three platforms you could inject an additional stage that was dependent on all three build stages. The approval would be required for this single stage, and then each of the subsequent deploy stages would be permitted to continue without further checks. The downside of this would be that you would have to wait for all builds to fail before you could deploy the app to any platform.

Summary

In this post we’ve looked at using a pipeline template for deploying Xamarin.Forms applications to AppCenter for testing across Windows (UWP), iOS and Android. In actual fact, and what I didn’t point out earlier, the deploy template can be used for any iOS, Android or Windows (UWP) app, not just a Xamarin.Forms application.

I also walked through setting up an environment so that you could add a manual approval step to the deployment process. Whilst the YAML pipelines don’t yet have all the features of the classic release pipeline, you can see from the way that the components connect and the UI that’s built in the portal that the foundations have been laid for future features to be built on.

Pipeline Templates: Building Xamarin.Forms Apps on Azure DevOps using Templates

One of the things I find frustrating is that for every new project we seem to have to recreate the build and release pipeline. In each case we step through the same steps, run into the same, albeit familiar, issues and end up with a pipeline that looks incredibly similar to the pipeline we setup … Continue reading “Pipeline Templates: Building Xamarin.Forms Apps on Azure DevOps using Templates”

One of the things I find frustrating is that for every new project we seem to have to recreate the build and release pipeline. In each case we step through the same steps, run into the same, albeit familiar, issues and end up with a pipeline that looks incredibly similar to the pipeline we setup for the last project we started. In this post I’m going to share the first set of Azure DevOps build templates that you can reuse in order to build your Xamarin Forms for iOS, Android and Windows.

Background

This started out as an experiment to consolidate three different build pipelines into a single build pipeline. Often for mobile applications developers resort to setting up a build pipeline for each target platform, and then potentially each target environment. For us this because untenable as we were working on a white-labelled product that would need to have a build pipeline for each offering, which would grow over time.

The first step was to leverage the ability in Azure DevOps to create multi-stage pipelines. I setup a different stage for each target platform. You might be asking, why did I set them up as different stages; couldn’t I have just created additional tasks, or perhaps even different jobs for the different platforms. Well, the nice thing about different stages is that they can use different build agents. This is of course an absolute must, since iOS needs to be built by a Mac agent, whilst Windows (ie UWP) needs to be built by a Windows build agent. I could have cheated and wrapped the Android build with either the iOS or Windows build to cut down on the build time but you’ll see in a bit why I kept them separate.

In addition, my initial goal for this experiment was to have a standard build template that we could use and that I could share with the community via my blog. However, I felt this was only a part solution – there are so many great posts out there on how to setup a build process for XYZ but they become stale the minute they’re published. What if we could create a set of templates that could be released, much like a product, and could evolve over time.

Pipeline Templates

In this post I’m introducing the Pipeline Templates github repository where I’ll be evolving build templates to help developers avoid the complexity of setting up the entire Azure DevOps, instead, focusing on building amazing applications. It’s still early days but check out the repository, watch and provide feedback so that we can evolve these templates as a community.

We’ll be starting with three templates that can be used to build a Xamarin.Forms application for iOS, Android and Windows. If we look at the basic structure of the repository, you’ll see that there’s a folder for Azure and a sub-folder for Mobile. The rationale was that over time this repository could house templates for other build platforms.

I also wanted to separate mobile app builds away from builds for the web or cloud services. However, the more I think about this the more I feel this separation is somewhat arbitrary because web apps built using SPA frameworks like React, Angular, Vue etc are similar in so many ways to mobile or desktop apps. I can imagine this structure will evolve over time – if you’re going to reference the templates in this repository, make sure you use the tags to ensure your build process doesn’t change or break as the templates evolve (more on this later).

Creating the Azure Build Pipeline

In order to use the pipeline templates you need to reference the GitHub repository as a resource within your build pipeline. I’ll walk through creating a new build pipeline but if you can add resources to an existing Yaml based pipeline using similar steps.

Start from the Pipelines tab in the Azure DevOps web portal and click the New Pipeline button (top right corner of the screen). Follow the steps to select your source code repository and pick a New pipeline template to use (I typically go with the Starter pipeline but either way we’re going to replace most of the yaml anyhow).

When you’re on the review tab, I suggest that you rename the yaml file. It’s not immediately obvious that you can change the name of the yaml file. The following image shows the default name on the left and then if you tap on the azure-pipelines.yml text you can actually edit it. On the right side of the image is the new filename and I’ve placed it in a pipelines folder to keep everything tidy in the repository.

Rather than making changes at the review tab, I just hit save (not the default “save and run” since we know the pipeline isn’t setup correctly).

Connecting to a GitHub Resource

According to the documentation you should be able to just add a GitHub resource directly to the Yaml file. However, in doing this I found that my pipeline didn’t work until I created an endpoint, similar to if you were to add other authenticated repositories as a resource.

Creating an endpoint is actually quite simple but you may well need additional permissions on your repository, depending on what access permissions you have. Click on Project Settings, in the bottom left corner of the Azure DevOps web portal, then click on the Service Connections tab. Click on the New service connection button, that’s in the top right corner of the Service connections screen. Select the GitHub connection type and then populate the New GitHub service connection flyout.

Once you click the Authorize button, you’ll most likely need to adjust the Service connection name – you’ll need this value in the next step.

The next step is to go back to your newly created pipeline (Note: if it doesn’t appear when you click on the Pipelines tab, you’ll need to switch view to All instead of Recent since you haven’t run your pipeline yet). Once you have the pipeline open, click Edit to bring up the yaml editor.

Replace the contents of the yaml file with the following:

resources:
  repositories:
    - repository: builttoroam_templates
      type: github
      name: builttoroam/pipeline_templates
      ref: refs/tags/v0.1.0
      endpoint: Pipeline-Templates

This block of yaml references the pipeline_templates repository that’s owned by builttoroam. It’s going to use the Pipeline-Templates service connection to access the repository where it’s going to look at tag v0.1.0. In the pipeline this resource can be referenced as builttoroam_templates, which is just a local name assigned to this resource by the pipeline.

Important: that you specify the ref attribute and specify a tag. If you don’t, you’ll be pointing to the latest available templates on master. Referencing master is fine as you setup your pipeline but you should change to referencing a tag release to make sure that as we change the repository, your build continues to work.

Xamarin.Forms Build Pipeline

Now that we’ve added a reference to the pipeline templates repository, we can make use of any of the templates. For example the following yaml references the ios Xamarin template.

stages:
- template:  azure/mobile/[email protected]_templates
  parameters:
    solution_filename: 'src/MyApp.sln'
    solution_build_configuration: 'Release'

In this case to access the template in the pipeline templates repository, we firstly need to use the @[local resource name] syntax and then secondly we need to provide the path to the yml file (ie azure/mobile).

This yaml snippet defines the stages for the build pipeline, including a stage that’s defined in the referenced template file (in this case build-xamarin-ios.yml). You can add additional stages by simply repeating the -template section and adjusting the name of the template.

This code snippet shows just the first two parameters being passed in. At the end of this post is a full example showing the required parameters. There are some additional parameters that can be used to adjust output file name and folder, as well as for adding custom steps to the beginning, pre and post build and at the end of the stage.

Full Example

The following is a fully worked example which includes iOS, Android and Windows. Note that there are a number of variables that are being passed into the templates. These are all defined within the Inspector XF Build Variables group, which can be defined via the Library tab in Azure DevOps.

resources:
  repositories:
    - repository: builttoroam_templates
      type: github
      name: builttoroam/pipeline_templates
      ref: refs/tags/v0.1.0
      endpoint: Pipeline-Templates
  
variables:
  - group: 'Inspector XF Build Variables'
  
stages:
- template:  azure/mobile/[email protected]_templates
  parameters:
    solution_filename: 'src/Apps/DotNet/Inspector.XF.sln'
    solution_build_configuration: 'Release'
    ios_plist_filename: 'src/Apps/DotNet/XF/InspectorXF/InspectorXF.iOS/Info.plist'
    ios_cert_password: '$(ios_signing_certificate_password)'
    ios_cert_securefiles_filename: '$(ios_signing_certificate_securefiles_filename)'
    ios_provisioning_profile_securefiles_filename: '$(ios_provisioning_profile_securefiles_filename)'
    build_number: '$(Build.BuildId)'
    full_version_number: '$(version_prefix).$(Build.BuildId)'
    ios_signing_identity: '$(ios_signing_identity)'
    ios_provisioning_profile_id: '$(ios_provisioning_profile_id)'

- template:  azure/mobile/[email protected]_templates
  parameters:
    build_android: $(android_enabled)
    solution_filename: 'src/Apps/DotNet/Inspector.XF.sln'
    solution_build_configuration: 'Release'
    android_manifest_filename:  'src/Apps/DotNet/XF/InspectorXF/InspectorXF.Android/Properties/AndroidManifest.xml'
    build_number: '$(Build.BuildId)'
    full_version_number: '$(version_prefix).$(Build.BuildId)'
    secure_file_keystore_filename: '$(android_keystore_filename)'
    keystore_alias: '$(android_keystore_alias)'
    keystore_password: '$(android_keystore_password)'

- template:  azure/mobile/[email protected]_templates
  parameters:
    solution_filename: 'src/Apps/DotNet/Inspector.XF.sln'
    solution_build_configuration: 'Release'
    uwpBuildPlatform: '$(uwpBuildPlatform)'
    windows_package_manifest_filename:  'src/Apps/DotNet/XF/InspectorXF/InspectorXF.UWP/Package.appxmanifest'
    build_number: '$(Build.BuildId)'
    full_version_number: '$(version_prefix).$(Build.BuildId)'
    windows_cert_securefiles_filename: '$(windows_signing_certificate_securefiles_filename)'
    windows_cert_password: '$(windows_signing_certificate_password)'

There are also some secure files that have been added to the Library, which again are referenced here using a variable defined in the variable group. The process for adding secure files is that you upload the file, give it a friendly name and then assign that friendly name to a variable that’s in the variable group.

Summary

In this post I’ve given you a very quick introduction to the Pipeline Templates repository. Over the coming posts I’ll walk through some of the templates and what you can do with them in more detail. I’d love feedback on the templates – raise an issue to suggest changes on the GitHub repository and I’ll see if I can encorporate them.

How to Support Multiple Environments in your Mobile Application?

Whether you’re developing an Android app in Kotlin, a cross-platform app in Flutter or Xamarin Forms, or an Xbox app in C#/XAML, supporting multiple environments when building an app, is just not as easy as it should be. For example the different environments might be dev, test, staging, prodution etc to align with your dev, … Continue reading “How to Support Multiple Environments in your Mobile Application?”

Whether you’re developing an Android app in Kotlin, a cross-platform app in Flutter or Xamarin Forms, or an Xbox app in C#/XAML, supporting multiple environments when building an app, is just not as easy as it should be. For example the different environments might be dev, test, staging, prodution etc to align with your dev, test and release process. Alternatively, you might have a white-labelled app that you can configure for a particular customer by adjusting some application settings. In each of these scenarios, it would be ideal to be able to deploy our application, along with a configuration, or settings, file. In this post we’re going to discuss why this isn’t possible, how this problem is typically solved, and then discuss an alternative approach to solving the problem.

Configuration Files

Before we jump in and discuss native applications, let’s take a look at a couple of scenarios where configuration files are already supported. The first example is a typical web applications that can be built once, and then released to an almost unlimited number of environments, where each one can have different settings, or attributes, applied using some form of a settings or configuration file. For example with an ASP.NET application you can specify application settings in the web.config or app.settings files. Alternatively if you’re deploying to an Azure App Service, you can configure various settings directly via the Azure portal (including overriding settings on a per-slot basis).

The use of configuration files isn’t isolated to web applications. In fact both WinForms and WPF applications can take advantage of the ConfigurationManager class in the .Net Framework to dynamically load configuration data from a file packaged alongside the application.

The introduction of application stores (eg the iOS App Store, Google Play Store and the Microsoft Store) brought with it the notion of an application package. Applications were packaged and then signed to ensure that what was received, and subsequently installed, on the device was the same package the publisher had submitted and that had been approved for distribution. None of the main stores support distributing a configuration file alongside the application, in the same way you could have done with a private distribution of a WinForms app.

Packaged Configuration Files

Given that it’s not possible to distribute a configuration file in parallel to the application, it is necessary to include configuration files within the application package. There are a couple of alternatives that you should consider when deciding on a configuration system.

Build Configuration Constants

This post by Jon that provides some background on what a build configuration is within Visual Studio and how to take advantage of it to control the behaviour of your application during development (Debug configuration) and in production (Release configuration).

Build configurations can define compilation constants that can be used to dynamically include or exclude code at compile time. The Debug build configuration typically already has the DEBUG constant defined but you can define your own. For example in the following image the DEV_ENV constant has been defined for the Debug build configuration.

In code, you can then use these constants to determine what code gets compiled. For example in the following code, when compiled with the Debug build configuration the DEV_ENV constant is defined, so the first definition of HelloText will be compiled. For all other build configurations, the DEV_ENV constant isn’t defined, so the second definition is compiled.

public static partial class Constants
{
#if DEV_ENV
    public const string HelloText = "Hello World - Dev Environment";
#else
    public const string HelloText = "Hello World";
#endif
}

You can extend this to include or exclude entire files by modifying the project file. There is no UI built into Visual Studio for doing this but the syntax of the csproj project file is relative simple, so not too hard to tweak. The following example demonstrates how to exclude two files (since all files are include by default within the project folder system), DebugConstants.cs and ReleaseConstants.cs, and then to selectively include them for the different build configurations.

<Project Sdk="Microsoft.NET.Sdk">
  ....
  <ItemGroup>
    <Compile Remove="DebugConstants.cs" />
    <Compile Remove="ReleaseConstants.cs" />
  </ItemGroup>
  <ItemGroup Condition="'$(Configuration)'=='Debug'">
    <Compile Include="DebugConstants.cs" />
  </ItemGroup>
  <ItemGroup Condition="'$(Configuration)'=='Release'">
    <Compile Include="ReleaseConstants.cs" />
  </ItemGroup>
  ....
</Project>

As you switch between Debug and Release build configurations in Visual Studio you can actually see the change in the Solution Explorer, showing which files will be included. In the following image the left screenshot of the Solution Explorer window shows that the DebugConstants.cs file has been included in the Debug configuration, whilst the right shows the ReleaseConstants.cs is included for the Release configuration.

Copy and Replace

In this post by Andrew he covers how you can include an app.settings file within your application. This is similar to the approach presented by Adam in his post on using Configuration Files in Xamarin.Forms.

The app.settings file can be replaced during the build process in order to switch between different environments. You can either choose to replace the entire app.settings file, or you can simply substitute individual key-value pairs.

Mobile Build Tools

Dan Siegel (of Prism notoriety) has developed some mobile build tools that he’s been working on to make it easier for developers to setup DevOps for mobile applications. I’d highly recommend integrating these tools into your build pipeline.

Build v Release Tasks for Multiple Environments

Ok, so before I wrap up this post I want to go back to the original premise I discussed. What I want to be able to do is to build my application once and then have different configurations for each environment. We can think of the devops for our application in two stages, Build and Release. The Build part of our process should do just that, it should build our application, and it should only have to build it once irrespective of what environment it’s going to target. The Release part of our process should augment the application configuration so that it targets the different environment.

The solutions presented so far have all resulted in the need to have different builds setup for each environments, so none of them present an ideal solution. The primary issue with applications is that the packaging format doesn’t support an external configuration file, so it’s not as simple as deploying a web application where you can simply change the configuration file.

To address this issue we need to look at how we can re-package our application during the release process, allowing us to modify a configuration file that’s included as part of the application package. More on this to come….

Running Android Device Tests for Xamarin.Essentials on Windows

As a developer working with Xamarin or Xamarin.Forms you should be aware of the Xamarin.Essentials package that Microsoft have been developing that “provides developers with cross-platform APIs for their mobile applications”. If you haven’t taken the time to look through the source code, it’s well worth cloning the repository and taking a look. Not only … Continue reading “Running Android Device Tests for Xamarin.Essentials on Windows”

As a developer working with Xamarin or Xamarin.Forms you should be aware of the Xamarin.Essentials package that Microsoft have been developing that “provides developers with cross-platform APIs for their mobile applications”. If you haven’t taken the time to look through the source code, it’s well worth cloning the repository and taking a look. Not only is this a great example of how to do multi-targeting, they’ve also taken the time to invest in device specific tests. In this post we’re going to look at how to run the device tests for Android in the same way that it’s run as part of the Azure Pipelines build script.

The first thing to do is of course clone a copy of the repository and then launch the solution in Visual Studio – before we try to run the device tests we’d better make sure everything is able to be built and run. Unless you’ve been using the same machine for a long time, you’re unlikely to have all the difference MonoAndroid SDKs installed. When you take a look at the Dependencies for Xamarin.Essentials in the Solution Explorer window it’s likely to look similar to what mine did.

The good news is that if you force a build, some of these are likely to go away. For example I actually did have 8.0 and 8.1 installed, so after the build they were resolved and the warning indicators disappeared. Unfortunately, the bad news is that I still had a couple of missing versions.

Luckily this is easily resolved using the Android SDK Manager (launch this from Tools > Android > Android SDK Manager from within Visual Studio), where you can install the missing SDK Platforms.

You’ll need to accept the license terms.

Installing the missing Android SDKs should resolve the Android dependencies. If you’re still seeing a warning for the UAP dependency, you need to verify that you’ve got the Windows SDK v 10.0.16299 installed.

The build process for Xamarin.Essentials is driven from the azure-pipeline.yml file in the root of the repository. This YAML file defines various jobs, one of which is the devicetests_android, which as the name suggests, runs the device tests. Actually this job invokes build.sh, specifying the test-android-emu target. The build.sh is actually just a proxy for running build.cake.

I wanted to run the device tests on Windows, so I’m going to be invoking build.ps1, which is the Powershell equivalent for invoking build.cake. The following command invokes Powershell, passes the build.ps1 as the Powershell script to invoke, and then I’ve included the other parameters that were specified in the azure-pipeline.yml.

powershell -f build.ps1 --target=test-android-emu --settings_skipverification=true --verbosity=diagnostic

Note: You need to invoke this command from the DeviceTests folder.

Unfortunately, simply running the Powershell script from a command prompt isn’t sufficient. There are a few steps you’ll need to jump through in order to get the script to run correctly. Without making any changes, if you run the Powershell script you’ll most likely see an error similar to the following image.

Whilst the error is highlighted in red, the actual cause of the error is in light grey font and indicates that JAVA_HOME hasn’t been defined. Easily fixed by using the set command (make sure the path to the JDK matches where it is on your computer).

set JAVA_HOME=C:\Program Files\Android\Jdk\microsoft_dist_openjdk_1.8.0.25

Also, whilst we’re setting environment variables, check that ANDROID_HOME and ANDROID_SDK_ROOT are set to the root folder of the Android SDK installation (typically C:\Program Files (x86)\Android\android-sdk on Windows if installed via Visual Studio installer)

Suggestion: You’ll find that invoking the Powershell script can be quite time consuming because Xamarin.Essentials has to be built (once for each supported platform) and then the emulator needs to be created and launched. To accelerate this process, after running the script the first time, you can temporarily disable the build by commenting out the “.IsDependentOn("Build-Android")” line in the build.cake file.

Now, when you run the Powershell script, you’re likely to see a different error. Again, it’s not the highlighted error that contains the useful information. Looking a few lines earlier you can see that there is an error relating the system image that the script attempts to use.

Again, fixing this issue is relatively easy. You just need to pick one of the existing system images (listed alongside the error) and use it to set the ANDROID_EMU_TARGET variable.

set ANDROID_EMU_TARGET=system-images;android-29;google_apis_playstore;x86

The next error you’ll see is at the point where the script attempts to launch the emulator. It will fail, indicating that it can’t find the file specified.

It would appear that the build script, build.cake, attempts to call “emulator.bat”. However, this file doesn’t exist. Instead, there is an emulator.exe – we just need to adjust the build.cake to use .exe instead of .bat when locating the emulator command.

Unfortunately there’s also an issue with the search logic, resulting in using a copy of emulator.exe that doesn’t work. After making the above change to .exe, you’ll see that it attempts to launch the emulator but comes up with an error “PANIC: Missing emulator engine program for ‘x86’ CPU”.

Again, this issue is easily fixed with a small change to build.exe to get it to search for the emulator.exe in the correct folder. In the following code, we’ve adjusted the search logic to look in only the emulator folder on Windows.

if (ANDROID_HOME != null) {
        var andHome = new DirectoryPath(ANDROID_HOME);
        if (DirectoryExists(andHome)) {
            if(IsRunningOnWindows()){
                emulatorPath = MakeAbsolute(andHome.Combine("emulator").CombineWithFilePath("emulator" + emulatorExt)).FullPath;
                if (!FileExists(emulatorPath))
                    emulatorPath = "emulator" + emulatorExt;
            }
            else{
                emulatorPath = MakeAbsolute(andHome.Combine("tools").CombineWithFilePath("emulator" + emulatorExt)).FullPath;
                if (!FileExists(emulatorPath))
                    emulatorPath = MakeAbsolute(andHome.Combine("emulator").CombineWithFilePath("emulator" + emulatorExt)).FullPath;
                if (!FileExists(emulatorPath))
                    emulatorPath = "emulator" + emulatorExt;
            }
        }
    }

Finally, when you run the script now, it will run through without error. However, you will see a Windows Security Alert requesting a rule be added to the firewall. You’ll need to click the Allow access button in order for the emulator to talk back to the build script – it uses a TCP listener to retrieve the results from the device tests.

After granting permissions, you should see the following output – if you want more details on the device test output, there’s an xml file that is returned which has the full details of the test execution.

One last comment: If you have the emulator running when you run the device test script, you’ll find that it generates an error at the end of running the script. When the script attempts to launch the emulator, emulator.exe will detect the running emulator and will exit immediately. At the end of the script it attempts to terminate the emulator.exe process – since this process has already ended, it throws and error. You can ignore this error as the device tests will still have executed correctly.

Content from Former Microsoft WPF and Silverlight Team Member

Someone pointed me in the direction of a series of great blog posts that have recently been migrated to a github repository. The posts are quite old, dating back to a period between 2005 and 2013 when Beatriz Stollnitz worked for Microsoft as part of the WPF and Silverlight teams. Here are the links to … Continue reading “Content from Former Microsoft WPF and Silverlight Team Member”

Someone pointed me in the direction of a series of great blog posts that have recently been migrated to a github repository. The posts are quite old, dating back to a period between 2005 and 2013 when Beatriz Stollnitz worked for Microsoft as part of the WPF and Silverlight teams. Here are the links to each of the posts – happy reading!

Update 11th August: I’ve forked the repository and have started to work my way through the sample code to a) update it to WPF on .NET 4.7.2 and b) provide a similar example for UWP + Uno (iOS, Android, WASM). Feel free to check it out and help out!

01-DataContext

02-EmptyBinding

03-GetListBoxItem

04-BindToComboBox

05-DisplayMemberPath

06-SelectedValue

07-ChangePanelItemsControl

08-BarGraph

09-CollectionViewSourceSample

10-MasterDetail

11-MasterDetailThreeLevels

12-DataBoundDialogBox

13-TemplatingItems

14-SortingGroups

15-GroupingTreeView

16-GroupByType

17-BoundListView

18-ThreeLevelMasterDetailADO

19-ObjectDataProviderSample

20-InsertingSeparators

21-CustomSorting

24-AsynchronousBinding

25-BindToEnum

26-DataTriggerSample

27-ConvertXaml

28-FilterSample

29-MultipleFilters

30-MultiBindingConverter

31-ChangesMultithreading

32-PolygonBinding

33-PolygonBinding2

34-PolygonBinding3

35-CommonQuestions

36-ADOIndependentView

37-PlanetsListBox

38-UpdateExplicit

39-TreeViewPerformancePart1

40-TreeViewPerformancePart2

41-TreeViewPerformancePart3

42-WPFPresenter

43-BindToXLinq

44-XLinqXMLMasterDetail

45-DebuggingDataBinding

46-DragDropListBox

47-ExpandTreeViewPart1

48-ExpandTreeViewPart2

49-ExpandTreeViewPart3

51-UIVirtualization

52-DataVirtualization

54-PieChartWithLabels

55-PieChartWithLabelsSilverlight

56-PieChartWithLabelsSilverlight

57-DataVirtualization

58-MultipleStyles

59-WPFCollectionViewSource

60-SLCollectionViewSource

61-OredevComputerWeekly

62-DataVirtualizationFiltering

64-DataVirtualizationFilteringSorting

66-SortingHierarchy

67-PieChartWithLabelsUpdates

69-BindRadioButtonsToEnumsPart1

70-BindRadioButtonsToEnumsPart2

71-BindRadioButtonsToEnumsPart3

72-BindRadioButtonsToEnumsPart4

73-BindRadioButtonsToEnumsPart5

74-PositioningDataBoundItems

75-SimultaneousEnableDisable

76-FocusWatcher

77-CaptureWatcher

78-BetterBindableBase

79-BooleanConverters

Using the UWP SplitView on iOS, Android and WebAssembly with Uno

In this post we’re going to cover one of the basics of app navigation which is the use of the UWP SplitView. If you’re coming from iOS and Android development you might be thinking “huh, I don’t even know what that is.” Well the good news is that it’s actually something you’re already familiar with. … Continue reading “Using the UWP SplitView on iOS, Android and WebAssembly with Uno”

In this post we’re going to cover one of the basics of app navigation which is the use of the UWP SplitView. If you’re coming from iOS and Android development you might be thinking “huh, I don’t even know what that is.” Well the good news is that it’s actually something you’re already familiar with. Whether you’re used to an app that has a master-details layout, or one that uses a burger menu to display a flyout menu, these can both be implemented using the UWP SplitView.

One thing to be aware of is that the UWP SplitView is one of the basic controls that was added early in the UWP lifecycle. Since then there have been other controls added, such as the NavigationView and the MasterDetailsView (Windows Community Toolkit) that provide extended functionality and are worth exploring depending on the requirements of your project.

New Project – Cross-Platform App (Uno Platform)

Let’s get into it – we’re going to start with creating a new project based on the latest Uno Visual Studio Extension (updated at end of July – if you’re using an older version I would recommend updating). I’m also working with the Visual Studio 2019 (16.3 preview 1.0) as this provides the best support for working with WebAssembly.

Cross-Platform App using the Uno Solution Template

Once you’ve created your solution, I would highly recommend that you a) update NuGet references and b) go through each platform and make sure you can build and run the application. However, as at the time of writing do NOT update the Microsoft.Extensions.Logging.* packages – there’s a note in the csproj files that states “Note that for WebAssembly version 1.1.1 of the console logger required.” My recommendation is to leave these packages at v1.1.1 across all projects. For the Uno libraries I pick the latest prerelease versions but be warned that this occasionally backfires as you may get an unstable version – this is why it’s important to run each platform before proceeding!!

Note: One issue I’m currently seeing in the Uno template is that it generates assets (i.e. images) with names that include the scale factor eg scale-200. This is fine for UWP but fails to build for Android. I’ve gone through and just removed the scale factor from the filenames. You’ll need to rebuild your UWP project to make sure it picks up the filename changes correctly.

Note 2: When running the WebAssembly (aka WASM) project, make sure you select “Start without Debugging” in Visual Studio.

Note 3: When running the iOS build you may need to set the Deployment Target in the Info.plist. If you’re using the latest preview of Visual Studio it will pick up iOS 12.4 on the build agent which will cause you app to fail to deploy if you haven’t set the Deployment Target.

Adding the UWP SplitView

Now that we have our new application up and running, it’s time to add the SplitView control.

Design in Blend

For this we’re going to switch over to Blend – yeh, the product still exists and you’ll see in a minute why we’re going to be using it. To make the switch, right-click on the MainPage.xaml in the shared project and select Design in Blend.

Switching to Design in Blend

Switching to Blend will take a few seconds, particularly if this is the first time you’ve ever opened Blend. You’ll also see errors as the iOS and Android projects won’t open – ignore these and don’t worry about the upgrade log that will popup (these are irritating but can be safely ignored as they won’t impact your project).

Despite selecting Design in Blend from the MainPage.xaml, once in Blend, you’ll actually need to use the Solution Explorer window to open MainPage.xaml in the designer. Once you’ve opened MainPage.xaml, Blend should feel fairly familiar, even if this is the first time you’ve used it. This is because a lot of the windows are shared with Visual Studio – the separation between the products is a mix of legacy (don’t want to disrupt the tiny percentage of developers who still use it) and the optimising that design tasks will be done using Blend.

Blend Designer

Adding the SplitView

There are a number of ways to add the SplitView control into the MainPage. If you’re familiar with writing XAML it’s probably just easiest to add the SplitView element. However, to build familiarity with Blend, let’s add the SplitView using the designer. In the Assets window, enter “splitview” to locate the SplitView control. Use the mouse to drag the SplitView down to the Objects and Timeline window at the position in the hierarchy where you want it to appear. In this case we’re going to drop it inside the existing Grid – I typically have a Grid as the root of most pages to allow setting of background and other properties using a application-wide style, independently of what content is nested within the Grid.

Adding the UWP SplitView

Adding the SplitView this way will add it as the second child of the Grid, immediately following the existing TextBlock. As we want the SplitView to take up the entire space of the page, we’re going to move the TextBlock in the main content area by dragging the TextBlock into the Grid that is nested within the SplitView. The XAML of the page should now look like the following:

<Page
    x:Class="UwpSplitViewSample.MainPage"
    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:local="using:UwpSplitViewSample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <SplitView>
            <SplitView.Pane>
                <Grid />
            </SplitView.Pane>
            <Grid>
                <TextBlock
                    Margin="20,20,20,20"
                    FontSize="30"
                    Text="Hello, world !" />
            </Grid>
        </SplitView>
    </Grid>
</Page>

The SplitView is made up of two distinct areas: the main content area and the pane that can be shown/hidden (i.e. the flyout). From this code we can see that the SplitView as two nested XAML elements which align to the SplitView areas. The first is the SplitView.Pane which defines the layout of the content that will appear within the pane/flyout of the SplitView. The second defines the layout of content to appear within the main content area of the SplitView, in this case a Grid with a TextBlock contained within it.

Adjusting Layout with Blend

I’ll repeat this: For those of us familiar with writing XAML, you’ll find that manually crafting XAML is by far the quickest way to add and edit elements. However, Blend provides a number of shortcuts for positioning elements that can come in handy. Some of which we’ll cover here.

Designing the SplitView Pane

For the purpose of this post, all we’re going to do is to add a StackPanel with a TextBlock and Button as children to both the SplitView.Pane and the SplitView. The TextBlock will be used to indicate what part of the SplitView it is (Pane or Content) and the Button will be used to Show and Hide the pane of the SplitView.

Let’s start with the SplitView.Pane which already has a Grid element. In the Objects and Timeline window, right-click on the Grid and select Change Layout Type, followed by StackPanel.

Change Layout Type to StackPanel

You should see the Grid change to StackPanel and remains selected in the Objects and Timeline window. Next, with the StackPanel still in focus, use the Assets window to locate the TextBlock and double-click on TextBlock to add a TextBlock as a child to the StackPanel. Repeat, this time for a Button.

We’re going to position the StackPanel in the centre of the SplitView pane. To do this, again keep the StackPanel selected in the Objects and Timeline window, we’re going to go to the Properties window and scroll down to the Layout section at set the HorizontalAlignment and VerticalAlignment properties to Center.

Centering the StackPanel

Whilst we’re using the Properties window, select the TextBlock and change the Text property to “Pane”, and then select the Button and change the Content property to “Hide Pane” (Note that unlike Xamarin.Forms that has a Text property, to set the text on a Button in UWP XAML you need to set the Content property).

The last thing we’re going to do with the XAML for the SplitView.Pane is to add an event handler to the Click event of the Button. With the Button selected in the Objects and Timeline window, go to the Properties window, and click on the lightning bold icon to switch from properties to events view. Locate the Click event and type the name of the event handler you want to create, in this case PaneButtonClick. When you press Enter the PaneButtonClick method will be created in the MainPage.xaml.cs code behind file and the XAML will be updated with the Click property.

Adding an Event Handler for the Click Event

For the moment, that’s where we’ll leave the XAML for the SplitView.Pane and we’ll implement the logic for the Click event handler shortly.

Designing the SplitView Content

The layout of the SplitView content is going to be very similar to that of the pane: A StackPanel with nested TextBlock and Button. However, we’re going to go about it slightly differently, considering the fact that we already have a TextBlock nested in a Grid in the content area. We’re going to start by wrapping the TextBlock in a StackPanel. To do this, right-click somewhere in the TextBlock on the designer and select Group Into, followed by StackPanel (this also works if you right-click on the TextBlock in the Objects and Timeline window).

After doing this the hierarchy of elements in the SplitView content will be Grid, StackPanel, TextBlock. However, if you look at the XAML you’ll notice that Blend has added some additional attributes, setting the Margin on the StackPanel and the Height and Width on the TextBlock. To remove these, we can just right-click the StackPanel and select Layout, followed by Reset All. Repeat this on the TextBlock.

From here we can repeat the remaining steps that we did for the SplitView pane:

  • Add a Button to the StackPanel
  • Set HorizontalAlignment and VerticalAlignment on the StackPanel to Center
  • Set Text on the TextBlock to “Content Area”
  • Set Content on the Button to “Show Pane”
  • Add an event handler called ShowPaneButtonClick to the Button Click event

The final XAML in MainPage.xaml should look similar to:

<Page x:Class="UwpSplitViewSample.MainPage"
      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:local="using:UwpSplitViewSample"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">
  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <SplitView>
      <SplitView.Pane>
        <StackPanel HorizontalAlignment="Center"
                    VerticalAlignment="Center">
          <TextBlock Text="Pane"
                     TextWrapping="Wrap" />
          <Button Click="PaneButtonClick"
                  Content="Hide Pane" />
        </StackPanel>
      </SplitView.Pane>
      <Grid>
        <StackPanel HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Orientation="Vertical">
          <TextBlock Text="Content Area" />
          <Button Click="ShowPaneButtonClick"
                  Content="Show Pane" />
        </StackPanel>
      </Grid>
    </SplitView>
  </Grid>
</Page>

DisplayMode Property on the UWP SplitView

The UWP SplitView has two properties that are worth exploring in a bit of detail and we’ll use these to control the behaviour, or should I say the layout, of the SplitView based on screen width. Let’s start with the easy one, which is the IsPaneOpen property. As you can imagine from the name, this property indicates whether the pane of the SplitView is currently open (i.e. displayed in full, rather than hidden or in compact mode), or not.

The second property worth looking at is the DisplayMode of the SplitView, and has the following four possible values:

  • Overlay – When the pane is visible it appears as an overlay across the content area. It doesn’t affect the size of the content area when it is shown/hidden.
  • Inline – When the pane is displayed it reduces the size of the content area by the width of the pane. As the pane is shown/hidden the width of the content area shrinks and grows accordingly.
  • CompactOverlay – Same as Overlay, except when the pane isn’t open, it still takes up space on the page equal to the CompactPaneLength property. Typically the compact view would show a list of icon buttons as a summary of the options available in the pane when it’s open.
  • CompactInline – Same as Inline, except when the pane isn’t open it still takes up space on the page equal to the CompactPaneLength property.

We’ll leave the use of the CompactOverlay and CompactInline options for the moment and focus on the other two options. The Overlay option is great for when the size of the application is narrow, for example on a mobile phone. The Inline option is better suited for when the application has sufficient width to allow the pane to be shown and there still to remain sufficient content area to be useful.

Using VisualState to Set DisplayMode

With this in mind, we’re going to use visual states to set the DisplayMode property based on the width of the application. For the purpose of this post we’re going to use a split point of 900, meaning that if the width of the application is greater than 900 the DisplayMode will be set to Inline. If it’s below 900 it’ll be set to Overlay.

To begin with, we’re going to set the default value of the DisplayMode to Compact by adding DisplayMode=”Compact” to the SplitView element. Next in Blend we’re going to add two visual states via the States window. In the States window, click the Add state group button and give the state group a name, SizeStateGroup.

Adding a Visual State Group

Next, click the Add state button twice to add two visual states and name them NarrowState and WideState. The actual names of the visual state group and the states are only for your benefit at this point, so you can name them according to what makes sense for you.

Adding a Visual State

Click on the WideState and you should see a red dot appear along side the state name. This indicates that Blend is in visual state editing mode. You should also see a red border appear around the main design area. Note that when you’re in visual state editing mode in Blend, any change you make via the tool windows (eg changing a property) will be tracked against the selected visual state.

Visual State Editing Mode

In visual state editing mode for the WideState, use the Properties window to change the DisplayMode to Inline. Next click the lightning icon button, Edit Adaptive Triggers, alongside the WideState visual state.

Adding an AdaptiveTrigger

In the Collection Editor: StateTriggers, set the MinWindowHeight to 0 and the MinWindowWidth to 900. This is adding a trigger which will return true when the width of the application exceeds 900. At this point the VisualStateManager will automatically switch to the WideState, without you having to explicitly run any code. Repeat the process of setting an adaptive trigger but this time for the NarrowState and set both MinWindowHeight and MinWindowWidth to 0.

The final XAML:

<Page x:Class="UwpSplitViewSample.MainPage"
      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:local="using:UwpSplitViewSample"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <SplitView x:Name="splitView"
               DisplayMode="Overlay">
      <SplitView.Pane>
        <StackPanel HorizontalAlignment="Center"
                    VerticalAlignment="Center">
          <TextBlock Text="Pane"
                     TextWrapping="Wrap" />
          <Button Click="PaneButtonClick"
                  Content="Hide Pane" />
        </StackPanel>
      </SplitView.Pane>
      <Grid>
        <StackPanel HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Orientation="Vertical">
          <TextBlock Text="Content Area" />
          <Button Click="ShowPaneButtonClick"
                  Content="Show Pane" />
        </StackPanel>
      </Grid>
    </SplitView>
    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup x:Name="SizeStateGroup">
       
        <VisualState x:Name="WideState">
          <VisualState.StateTriggers>
            <AdaptiveTrigger MinWindowHeight="0"
                             MinWindowWidth="900" />
          </VisualState.StateTriggers>
          <VisualState.Setters>
            <Setter Target="splitView.(SplitView.DisplayMode)" Value="Inline" />
          </VisualState.Setters>
        </VisualState>
        <VisualState x:Name="NarrowState">
          <VisualState.StateTriggers>
            <AdaptiveTrigger MinWindowHeight="0"
                             MinWindowWidth="0" />
          </VisualState.StateTriggers>
        </VisualState>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
  </Grid>
</Page>

Note: In UWP the order of the VisualState elements doesn’t matter as all the AdaptiveTriggers are evaluated and the best match is determined. For example if the application width is 1000, the conditions for the triggers for both NarrowState and WideState are met. However, the WideState is a better match since the width is > 900. For Uno (i.e. iOS, Android and WebAssembly), the order matters – the first trigger to match, wins. In the above XAML you’ll see the WideState before the NarrowState to make sure that if the application width is > 900, the WideState visual state is active.

Opening and Closing the UWP SplitView Pane

If you run the application at this point and adjust the width of the application the DisplayMode property will switch between Inline and Overlay. However, you won’t notice any change because we currently don’t have a way to open the pane. To do this we’ll add some code to the Click event handlers we created earlier, as follows:

private void PaneButtonClick(object sender, RoutedEventArgs e)
{
    splitView.IsPaneOpen = false;
}
private void ShowPaneButtonClick(object sender, RoutedEventArgs e)
{
    splitView.IsPaneOpen = true;
}

UWP SplitView In Action

With all the XAML and code in place, let’s see it in action on all various platforms.

UWP with Resizing Application
Opening the Pane on WASM
Auto-dismiss Flyout on Android

In this post we’ve covered off how easily you can build a responsive interface using Blend and the UWP SplitView. The fact that it then just works on iOS, Android and WebAssembly is a big bonus thanks to the Uno platform.

Tutorial: How to Create a XAML Templated Control

In this post we’re going to cover creating a custom control that uses a control template to define how it looks, aka a Templated Control. The principles of templated, or lookless, controls have been adopted by most of the XAML based technologies but for the purpose of this post we’re going to start by building … Continue reading “Tutorial: How to Create a XAML Templated Control”

In this post we’re going to cover creating a custom control that uses a control template to define how it looks, aka a Templated Control. The principles of templated, or lookless, controls have been adopted by most of the XAML based technologies but for the purpose of this post we’re going to start by building for Windows (ie UWP) and then we’re going to leverage Uno to deliver the same control across iOS, Android and even the web using Web Assembly (WASM).

Full source code available on GitHub

Disclaimer: The purpose of this post is to walk through the process of creating a Templated Control. To do this we’re going to create a multi-switch control (i.e. a switch that has multiple positions). However, I haven’t attempted to win any design awards with this control. In fact the entire point of a Templated Control is that it’s possible to restyle the control and add animation etc without changing the basic functionality of the control.

Getting Started – Uno Project Templates

To get started a bit of house keeping – let’s make sure we have our project setup so that we can build a Templated Control in its own library (so we can reuse it) and that we have a set of head projects where we can test out our control. Since we’re going to use Uno to take our control cross platform, we’ll use the Uno project templates to get us started.

We’ll start by creating a new project based on the Cross-Platform App (Uno Platform). If you don’t have the Uno project templates installed you can grab the Uno Platform Solution Templates Visual Studio extension from the marketplace.

Set some basic project information – in this case our head projects are just for the purpose of testing our Templated Control so we’ve named it XAMLControlSample.

Once you’ve completed creating the new project you should have a solution with four head projects (iOS, Android, UWP and WASM) as well as a shared project. The XAML for the MainPage is in the shared project, which is where we’ll be adding an instance of our Templated Control to test it out after we’ve created it.

Speaking of which, we need to create a library for our Templated Control to reside in. We’ll add another project, this time based on the Cross-Platform Library (Uno Platform) project template. If you’re not interested in taking your Templated Control cross platform (i.e. you’re just building for UWP) you can simply create a class library based on the Class Library (Universal Windows) project template. The big difference with the Uno template is that it creates a project that is setup with multi-targeting, meaning that it will create a library that will have an iOS, Android, Windows and WASM binaries.

We’ll give our class library a name, in this case MyCustomControls.

The next step is to create our Templated Control. Unfortunately due to the limited support for multi-targeting within Visual Studio, if you attempt to add a new item directly to the class library, you won’t see any of the Windows Universal item templates. Instead what we need to do is to create the Template Control in the UWP head project and move the relevant files across to the class library. Right-click on the UWP head project and select Add, New Item. In the Add New Item dialog, select the Templated Control item template and give the control a name, in this case MultiSwitchControl.

After adding the Templated Control you should see two files added to the UWP head project: Generic.xaml (in the Themes folder) and MultiSwitchControl.cs (you Templated Control). Note that there’s no XAML file for the Templated Control (i.e. there’s no MultiSwitchControl.xaml), which you would get if you were creating a UserControl. This is because the XAML that defines how the Templated Control looks is all contained in the Style and the associated ControlTemplate.

The final piece of setup is just to move these two files, including the Themes folder, into the class library. After moving the files, you should make sure that you update the namespace of your Templated Control to reflect the correct project. In my case I had to change the namespace from XAMLControlSample to MyCustomControls.

After moving the Templated Control to its correct location, let’s make sure that it can be consumed by each of our head projects:

  • Update NuGet packages, importantly the Uno packages
  • For each head project add a reference to the MyCustomControls project.
  • Build and run each head project to make sure no compile errors (Note for WASM use the “Start without Debugging” option to launch the browser)

Once we’ve confirmed that each platform works without our Templated Control, it’s time to add an instance to the MainPage. Update the MainPage code to the following:

<Page x:Class="XAMLControlSample.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:ctrls="using:MyCustomControls">
  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <StackPanel>
      <TextBlock Margin="20"
                 HorizontalAlignment="Center"
                 Text="XAML Templated Control"
                 FontSize="30" />
      <ctrls:MultiSwitchControl 
                                Width="400"
                                Height="400"
                                Background="Blue" />
    </StackPanel>
  </Grid>
</Page>

Run each of the head projects and verify that the MultiSwitchControl appears as a blue square.

Breaking Down the Templated Control

In the previous section we walked through creating a very simple Templated Control and demonstrated that through the power of Uno the same control can be used across iOS, Android, Windows and Web. Let’s take a look at how the Templated Control works, before we move on to building out our multi-switch control.

DefaultStyleKey for Implicit Style Lookup

The MultiSwitchControl.cs code file contains very little code. In fact, the only code it contains by default is a parameterless constructor that sets the DefaultStyleKey property.

public MultiSwitchControl()
{
    this.DefaultStyleKey = typeof(MultiSwitchControl);
}

What’s not apparent here is that setting the DefaultStyleKey is critical to the loading of the control. When an instance of the MultiSwitchControl is created, unless the Style attribute has been set, the framework looks for a corresponding implicit style. An implicit style is one that doesn’t have an explicit Key defined. Instead, the Key for an implicit style is essentially the TargetType of the Style. For example in the Generic.xaml you’ll see that there is a Style defined with TargetType set to MultiSwitchControl.

<Style TargetType="local:MultiSwitchControl" >
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:MultiSwitchControl">
                <Border
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

What’s important to note is that when the framework looks for the implicit Style, it doesn’t just assume that it should look for a Style with the TargetType matching that of the control. Instead it looks at the Type returned by the DefaultStyleKey property. Whilst this property is often just set to the Type of the control, there are cases where this isn’t the case.

Making your Implicit Style Explicit

One thing that annoys me about the item template that is used to generate the Templated Control is that it only defines an implicit Style for the control. The weakness of this is that it means that any developer wanting to override the Style has to copy the entire Style into their application. A better alternative is to make your Style explicit by giving it a Key, thus making it possible for other developers to inherit from your Style using the BasedOn attribute.

Of course, if you make your Style explicit, your Templated Control will no longer be able to find the Style without you explicitly referencing it. This is simple to overcome by defining an implicit style that inherits from your explicit Style.

If this all sounds a little complex, check out the amended Styles for the MultiSwitchControl below (there’s no code changes required to the MultiSwitchControl itself since it still relies on the implicit Style).

<Style x:Key="MultiSwitchControlDefaultStyle"
     TargetType="local:MultiSwitchControl">
<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="local:MultiSwitchControl">
      <Border Background="{TemplateBinding Background}"
              BorderBrush="{TemplateBinding BorderBrush}"
              BorderThickness="{TemplateBinding BorderThickness}" />
    </ControlTemplate>
  </Setter.Value>
</Setter>
</Style>
<Style TargetType="local:MultiSwitchControl"
     BasedOn="{StaticResource MultiSwitchControlDefaultStyle}" />

Designing the Template Control

At this point we have a lot of the infrastructure in place so we can get on with actually building our Templated Control. In this case we’re building a four-way switch control. It actually has five states: Off (Center), Up, Right, Down, Left, and as mentioned earlier we’re going to put minimal effort into the default design/layout of the control. We’ll show at the end of the process how easy it is for a developer consuming the control to override the Style and provide their own design without having to re-code the operation of the control (i.e. a true lookless control).

Simple Box Layout for the Template Control

To keep things simple the layout for the multi-switch that we’ll add to the MultiSwitchControlDefaultStyle will be a cross based on a 5×5 grid. There will be a box defined in the middle of the top row (Up), the center of the fifth column (Right), the middle of the bottom row (Down), the center of the first column (Left) and at the intersection of the third row and third column (Off). We’ve used a 5×5 layout to give a bit of spacing between the boxes, as you can see from the following image.

The updates Style defines each box using a Grid. At this stage a Border element would have sufficed. However, as you’ll see in the next step we’ll be nesting a couple of elements in the box to provide the visual context for when the user moves the mouse over the box, presses or clicks on the box, and when the box is selected.

<Style x:Key="MultiSwitchControlDefaultStyle"
     TargetType="local:MultiSwitchControl">
<Setter Property="BorderBrush" Value="SteelBlue" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="local:MultiSwitchControl">
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition />
          <ColumnDefinition />
          <ColumnDefinition />
          <ColumnDefinition />
          <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition />
          <RowDefinition />
          <RowDefinition />
          <RowDefinition />
          <RowDefinition />
        </Grid.RowDefinitions>
        <Grid x:Name="PART_Off"
              Grid.Row="2"
              Grid.Column="2"
              Background="Transparent"
              BorderBrush="{TemplateBinding BorderBrush}"
              BorderThickness="{TemplateBinding BorderThickness}" />
        <Grid x:Name="PART_Up"
              Grid.Row="0"
              Grid.Column="2"
              Background="Transparent"
              BorderBrush="{TemplateBinding BorderBrush}"
              BorderThickness="{TemplateBinding BorderThickness}" />
        <Grid x:Name="PART_Right"
              Grid.Row="2"
              Grid.Column="4"
              Background="Transparent"
              BorderBrush="{TemplateBinding BorderBrush}"
              BorderThickness="{TemplateBinding BorderThickness}" />
        <Grid x:Name="PART_Down"
              Grid.Row="4"
              Grid.Column="2"
              Background="Transparent"
              BorderBrush="{TemplateBinding BorderBrush}"
              BorderThickness="{TemplateBinding BorderThickness}" />
        <Grid x:Name="PART_Left"
              Grid.Row="2"
              Grid.Column="0"
              Background="Transparent"
              BorderBrush="{TemplateBinding BorderBrush}"
              BorderThickness="{TemplateBinding BorderThickness}" />
      </Grid>
    </ControlTemplate>
  </Setter.Value>
</Setter>
</Style>

Note that in most cases where there is repeated XAML (for example setting the properties of Background, BorderBrush and BorderThickness) it pays to extract these into a Style that can simply be applied to all elements. However, in practice this both adds to the overhead of loading the control and you immediately run into limitations on the TemplateBinding markup extension. Attempting to extract these elements to a Style will result in a runtime exception that doesn’t seem to have a clear work around.

The next thing to note about the Style is that we’ve added a Setter for both BorderBrush and BorderThickness. The Setters define the default values for these properties, meaning that if the developer doesn’t explicitly set them on their instance of the MultiSwitchControl they’ll still have a value. If we didn’t do this, the default appearance of the MultiSwitchControl wouldn’t show the boxes since there would be no brush, and thickness would be 0.

The last thing to note about the Style is that each of the Grid elements have a Name attribute. In each case the value has the prefix “PART_” followed by the corresponding switch state eg PART_Off. This prefix was a convention adopted by WPF but subsequently dropped for Silverlight (see this post for some commentary on this topic), Windows Phone, UWP etc. Whilst you don’t have to adopt this prefix (you’ll see why in a minute) I still find it quite a clean way to identify parts of the Style that have to be there in order for the control to function correctly.

Visual States for the Templated Control

As mentioned earlier we want our Templated Control to be able to provide contextual feedback to the user. There are three things that we want to be able to do:

  • Indicate when the user moves the mouse (UWP & WASM) over a box
  • Indicate when the user clicks, presses, touches into a box
  • Indicate when the user has selected a box

The first two of these we’ll pair together as they can represent the current state of the input device (aka pointer). This will be our CommonStates VisualStateGroup, to be consistent with other Windows controls, and will contain the following Visual States:

  • Normal – pointer isn’t over any element or pressed down on any element
  • PointerOverXXX – pointer has entered the area of element XXX
  • PressedXXX – pointer has been pressed down on element XXX

Element XXX will be one of the Grid elements named in our Style, so our states will be PointerOverOff and PressedOff for the PART_Off Grid.

To track which box is currently selected we’ll create a second VisualStateGroup called SelectionStates, which will include Visual States with the naming convention SelectionXXX. So for the PART_Off Grid there will be a corresponding VisualState called SelectionOff. Additionally there will be one extra VisualState, SelectionNone, which represents the default state where no box has focus.

You might be asking at this point – why the need for two VisualStateGroups? or why not three? The answer to this is that VisualStateGroups should define mutually exclusive VisualStates; and that VisualStates from one group should not set the same properties as VisualStates from a different group. If we look at the scenarios above it’s very clear that we’d want to be able to specify which box is currently selected whilst being able to highlight a different box that the user may have moused over. What’s not immediately clear is why we’ve combined the PointerOver and the Pressed states into the one group. The reality is that we could have separated these into a third group. However, in this case we’re going to keep the implementation simple by assuming that the state of the pointer will either be PointerOver or Pressed and not both at the same time.

I mentioned earlier that each of the Grids we created for the different switch states were going to contain multiple elements. In fact we’re going to add three Border elements to each, with the resulting Grids all being similar to the following Part_Off Grid, where the element names have the switch state as their prefix eg OffPointerOver, OffPressed, OffSelection.

<Grid x:Name="PART_Off"
      Grid.Row="2"
      Grid.Column="2"
      Background="Transparent"
      BorderBrush="{TemplateBinding BorderBrush}"
      BorderThickness="{TemplateBinding BorderThickness}">
  <Border x:Name="OffPointerOver"
          Background="{TemplateBinding Background}"
          Visibility="Collapsed" />
  <Border x:Name="OffPressed"
          Opacity="{TemplateBinding PressedOpacity}"
          Background="{TemplateBinding Foreground}"
          Visibility="Collapsed" />
  <Border x:Name="OffSelection"
          Background="{TemplateBinding Foreground}"
          Visibility="Collapsed" />
</Grid>

Each Border has its Visibility property set to Collapsed. The OffPointerOver Border will be set to Visible when a Pointer enters the region of PART_Off. The OffPressed will be set to Visible when a Pointer is pressed inside the PART_Off. Lastly, the OffSelection will be set to Visible when the PART_Off is selected (i.e. the state of the switch is set to Off). All this of course has to be done with the corresponding visual states, as follows:

<VisualStateManager.VisualStateGroups>
  <VisualStateGroup x:Name="CommonStates">
    <VisualState x:Name="Normal" />
    <VisualState x:Name="PointerOverOff">
      <VisualState.Setters>
        <Setter Target="OffPointerOver.Visibility" Value="Visible" />
      </VisualState.Setters>
    </VisualState>
    ...
    <VisualState x:Name="PressedOff">
      <VisualState.Setters>
        <Setter Target="OffPressed.Visibility" Value="Visible" />
      </VisualState.Setters>
    </VisualState>
    ...
  </VisualStateGroup>
  <VisualStateGroup x:Name="SelectionStates">
    <VisualState x:Name="SelectionNone" />
    <VisualState x:Name="SelectionOff">
      <VisualState.Setters>
        <Setter Target="OffSelection.Visibility" Value="Visible" />
      </VisualState.Setters>
    </VisualState>
    ...
  </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

The visual states for the other parts are all similar, just with different names in the Target of the Setter.

Building the Functionality of the Templated Control

So far we’ve focused on getting the basic layout of the Templated Control sorted. This has included specifying the different visual states that map to both user interaction (i.e. pointer over and pressed) as well as the switch states (i.e. selection). What’s missing is that actual functionality of the MultiSwitchControl which will trigger the changes in the visual states and track what the current switch state is.

Current Switch State

To track the current state of the switch I’m going to define an enum called SwitchState, which will include the values Off, Up, Right, Down and Left. For completion I’ve added a None state to represent an invalid or non-set state. I’ll then add a Value dependency property which will track the current state of the switch. when the Value does change, the ValuePropertyChanged method will be invoked, which subsequently calls the UpdateSwitchState that is responsible for calling GoToState on the VisualStateManager. The name of the new VisualState is specified by concatenating the prefix “Selection” with the current switch Value. For example if the current Value is SwitchState.Off, the visual state name would be SelectionOff.

public enum SwitchState
{
    None,
    Off,
    Up,
    Right,
    Down,
    Left
}

private const string SelectionStatePrefix = "Selection";

public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register(nameof(Value), typeof(SwitchState), 
                                typeof(MultiSwitchControl), 
                                new PropertyMetadata(SwitchState.None, ValuePropertyChanged));

public SwitchState Value
{
    get => (SwitchState)GetValue(ValueProperty);
    set => SetValue(ValueProperty, value);
}

private static void ValuePropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
{
    var switchControl = dependencyObject as MultiSwitchControl;
    switchControl?.UpdateSwitchState();
}

private void UpdateSwitchState()
{
    VisualStateManager.GoToState(this, SelectionStatePrefix + this.Value, true);
}

Pointer Events in the Templated Control

A lot of the visual state changes are conditional on intercepting pointer activity entering, exiting, pressing and release on the Templated Control. To attach the correct event handlers we need to override the OnApplyTemplate method – this method is called to apply the template to the control, afterwhich the various parts of the template are available to interact with.

private IDictionary<UIElement, (SwitchState state, bool isInside, bool isPressed)> Parts { get; } = new Dictionary<UIElement, (SwitchState state, bool isInside, bool isPressed)>();

protected override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    var switchStates = new[] { SwitchState.Off, SwitchState.Up, SwitchState.Right, SwitchState.Down, SwitchState.Left };
    foreach (var s in switchStates)
    {
        SetupPart(s);
    }

    Value = SwitchState.Off;
}

private void SetupPart(SwitchState state)
{
    var partName = PartPrefix + state;
    var partOff = GetTemplateChild(partName) as UIElement;
    if (partOff == null) throw new NullReferenceException($"{partName} expected in control template");
    Parts[partOff] = (state: state, isInside: false, isPressed: false);
    partOff.PointerPressed += PartPointerPressed;
    partOff.PointerReleased += PartPointerReleased;
    partOff.PointerEntered += PartPointerEntered;
    partOff.PointerExited += PartPointerExited;
}

As the code above illustrates, the OnApplyTemplate method iterates through a list of switch states, invoking the SetupPart method, afterwhich it sets the default value of the switch to Off. The SetupPart method calls GetTemplateChild to retrieve the element generated by the corresponding template part. For example for the SwitchState.Off, the partName is “PART_Off”. Calling GetTemplateChild doesn’t retrieve the Grid from the ControlTemplate, it retrieves the Grid that was created as part of applying the ControlTemplate to the instance of the MultiSwitchControl.

The Parts dictionary is used to track the current state of each part of the MultiSwitchControl. More specifically it tracks whether a pointer is inside the part and whether the pointer has been pressed. As you’ll see in the next code snippet, these values are used to determine when different visual state changes are applied.

At this point we also wire up the event handlers for each of the pointer events. The expected flow is that a pointer will enter the part, it may then be pressed (which will capture the pointer), the pointer may then exit and/or release at some point in the future. If the pointer is released whilst still within the part, this will select the part and change the state of the MultiSwitchControl.

private void PartPointerEntered(object sender, PointerRoutedEventArgs e)
{
    var partElement = sender as UIElement;
    if (partElement == null)
    {
        return;
    }

    var part = Parts[partElement];
    Parts[partElement] = (part.state, true, part.isPressed);
    if (!part.isPressed)
    {
        VisualStateManager.GoToState(this, PointerOverStatePrefix + part.state, true);
    }
}

private void PartPointerExited(object sender, PointerRoutedEventArgs e)
{
    var partElement = sender as UIElement;
    if (partElement == null)
    {
        return;
    }

    var part = Parts[partElement];
    Parts[partElement] = (part.state, false, part.isPressed);
    if (!part.isPressed)
    {
        VisualStateManager.GoToState(this, NormalState, true);
    }
}

private void PartPointerPressed(object sender, PointerRoutedEventArgs e)
{
    var partElement = sender as UIElement;
    if (partElement == null)
    {
        return;
    }

    var part = Parts[partElement];
    if (!part.isInside && !part.isPressed)
    {
        // Hack to deal with Android not firing events correctly
        //VisualStateManager.GoToState(this, "Selection" + part.state, true);
        Value = part.state;
        VisualStateManager.GoToState(this, NormalState, true);
        return;
    }
    Parts[partElement] = (part.state, part.isInside, true);
    VisualStateManager.GoToState(this, PressedStatePrefix + part.state, true);
    partElement.CapturePointer(e.Pointer);
}

private void PartPointerReleased(object sender, PointerRoutedEventArgs e)
{
    var partElement = sender as UIElement;
    if (partElement == null)
    {
        return;
    }

    partElement.ReleasePointerCaptures();
    var part = Parts[partElement];
    Parts[partElement] = (part.state, part.isInside, false);
    if (part.isInside)
    {
        Value = part.state;
    }
    VisualStateManager.GoToState(this, NormalState, true);
}

What’s a TemplatePart?

Earlier in this post I mentioned that WPF had a pseudo standard for the naming of parts of the template that needed to exist. The more precise name for these elements are template parts and the reason that the naming convention is no longer widely adopted is that there is a more prescriptive way to communicate to developers the required parts of a control.

The TemplatePartAttribute should be used to define the name and, if necessary, the type of the content template that need to exist in order for the control to operate correctly. In the case of the MultiSwitchControl there are five template parts, so we add five instances of the TemplatePartAttribute to the MultiSwitchControl class.

[TemplatePart(Name = "PART_Off")]
[TemplatePart(Name = "PART_Up")]
[TemplatePart(Name = "PART_Right")]
[TemplatePart(Name = "PART_Down")]
[TemplatePart(Name = "PART_Left")]
public partial class MultiSwitchControl : Control

I’d love to stay that these attributes showed up in the visual designer in Visual Studio or Blend but the reality is that both designers are in a pretty messed up state right now, so I would count on getting any useful prompts. The best advice I’d give is that if you’re going to start messing with the template of a control, inspect the class for yourself and see what template parts are required.

Are We There Yet?

Yes, the good news is that we’ve got to a point where we have a functioning control. We’ve used all the power of UWP to separate the visuals (i.e. the ControlTemplate coupled with Visual States) from the underlying control functionality. The only real connection is via the named parts of the template.

The following GIFs illustrate the control running on Windows, Android and WASM:

Overriding the Style of a Templated Control

The last thing I wanted to illustrate is how it’s possible to adjust the layout and visual appearance of the switch control without impacting the way it works. In the App.xaml file in the shared project (i.e. not in the class library) I’ve copied across the Style for the MultiSwitchControl. I’ve subsequently modified the ControlTemplate as follows:

  • Instead of multiple rows, all the boxes are now placed in 1 row
  • Each box now has a rounded corner, effectively causing them to be circular in shape (this was admittedly a lazy way and I should really have made them ellipses).
<ControlTemplate TargetType="myCustomControls:MultiSwitchControl">
<Grid Background="Transparent"
      DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
  <Grid.ColumnDefinitions>
    <ColumnDefinition />
    <ColumnDefinition />
    <ColumnDefinition />
    <ColumnDefinition />
    <ColumnDefinition />
    <ColumnDefinition />
    <ColumnDefinition />
    <ColumnDefinition />
    <ColumnDefinition />
  </Grid.ColumnDefinitions>
  <Grid x:Name="PART_Left"
        Grid.Column="0"
        Background="Transparent"
        CornerRadius="30"
        BorderBrush="{TemplateBinding BorderBrush}"
        BorderThickness="{TemplateBinding BorderThickness}">
    <Border x:Name="PART_Left_PointerOver"
            Background="{TemplateBinding Background}"
            Visibility="Collapsed" />
    <Border x:Name="PART_Left_Pressed"
            Opacity="{TemplateBinding PressedOpacity}"
            Background="{TemplateBinding Foreground}"
            Visibility="Collapsed" />
    <Border x:Name="PART_Left_Selection"
            Background="{TemplateBinding Foreground}"
            Visibility="Collapsed" />
  </Grid>
  <Grid x:Name="PART_Down"
        Grid.Column="2"
        Background="Transparent"
        CornerRadius="30"
        BorderBrush="{TemplateBinding BorderBrush}"
        BorderThickness="{TemplateBinding BorderThickness}">
    <Border x:Name="PART_Down_PointerOver"
            Background="{TemplateBinding Background}"
            Visibility="Collapsed" />
    <Border x:Name="PART_Down_Pressed"
            Opacity="{TemplateBinding PressedOpacity}"
            Background="{TemplateBinding Foreground}"
            Visibility="Collapsed" />
    <Border x:Name="PART_Down_Selection"
            Background="{TemplateBinding Foreground}"
            Visibility="Collapsed" />
  </Grid>
  <Grid x:Name="PART_Off"
        Grid.Column="4"
        Background="Transparent"
        CornerRadius="30"
        BorderBrush="{TemplateBinding BorderBrush}"
        BorderThickness="{TemplateBinding BorderThickness}">
    <Border x:Name="PART_Off_PointerOver"
            Background="{TemplateBinding Background}"
            Visibility="Collapsed" />
    <Border x:Name="PART_Off_Pressed"
            Opacity="{TemplateBinding PressedOpacity}"
            Background="{TemplateBinding Foreground}"
            Visibility="Collapsed" />
    <Border x:Name="PART_Off_Selection"
            Background="{TemplateBinding Foreground}"
            Visibility="Collapsed" />
  </Grid>
  <Grid x:Name="PART_Up"
        Grid.Column="6"
        Background="Transparent"
        CornerRadius="30"
        BorderBrush="{TemplateBinding BorderBrush}"
        BorderThickness="{TemplateBinding BorderThickness}">
    <Border x:Name="PART_Up_PointerOver"
            Background="{TemplateBinding Background}"
            Visibility="Collapsed" />
    <Border x:Name="PART_Up_Pressed"
            Opacity="{TemplateBinding PressedOpacity}"
            Background="{TemplateBinding Foreground}"
            Visibility="Collapsed" />
    <Border x:Name="PART_Up_Selection"
            Background="{TemplateBinding Foreground}"
            Visibility="Collapsed" />
  </Grid>
  <Grid x:Name="PART_Right"
        Grid.Column="8"
        Background="Transparent"
        CornerRadius="30"
        BorderBrush="{TemplateBinding BorderBrush}"
        BorderThickness="{TemplateBinding BorderThickness}">
    <Border x:Name="PART_Right_PointerOver"
            Background="{TemplateBinding Background}"
            Visibility="Collapsed" />
    <Border x:Name="PART_Right_Pressed"
            Opacity="{TemplateBinding PressedOpacity}"
            Background="{TemplateBinding Foreground}"
            Visibility="Collapsed" />
    <Border x:Name="PART_Right_Selection"
            Background="{TemplateBinding Foreground}"
            Visibility="Collapsed" />
  </Grid>
  <VisualStateManager.VisualStateGroups>
    ...
  </VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>

The only other change I needed to make was in MainPage I needed to change the instance of the MultiSwitchControl to reference the Style that I’d added. Now when I run my sample application I can see that the MultiSwitchControl looks dramatically different, and yet still functions the same way.

Wrapping up the Templated Control

As you’ve hopefully seen in this post there’s huge potential with a Templated Control to build a component that can be heavily reused and more importantly restyled. The point of Templated Controls, or lookless controls, is that the restyling shouldn’t change the core functionality.

What excites me about the Uno platform is that this stuff just works. The entire Templated Control I’ve walked through works on Android, iOS, Windows and WASM – what other technology allows you to do that, with the same ability to retemplate a control.

Don’t forget the full source code is available on GitHub

Optimising Multi-Targeting with Visual Studio Solution Filters

Over time Visual Studio has progressively improved support for solutions that have a large number of projects. MvvmCross used to be over 200 projects to handle each of the target platforms it supports. With the introduction of multi-targeted projects the number of projects dropped significantly to around 50 projects. For example, the core MvvmCross project … Continue reading “Optimising Multi-Targeting with Visual Studio Solution Filters”

Over time Visual Studio has progressively improved support for solutions that have a large number of projects. MvvmCross used to be over 200 projects to handle each of the target platforms it supports. With the introduction of multi-targeted projects the number of projects dropped significantly to around 50 projects. For example, the core MvvmCross project has 10 target framework monikers (TFM), instead of having a separate project for each framework. However, the decrease in load time (due to fewer projects) was offset by an increase in build time. Rebuilding the MvvmCross project triggers a build for each TFM, so the project is built 10 times. In this post, I’ll walk through a couple of techniques we use to reduce the build time when working with MvvmCross.

Whilst MvvmCross supports a wide range of target platforms, the reality is that when I’m making changes to MvvmCross I’ll be doing most of the work against one platform. For example being on Windows I might pick Android or Windows and use either the Playground or Playground.Forms sample apps to run up and test my changes. Since I’m working with only one platform at any given time, I don’t need every target framework to be built every time I make a change. Unfortunately there’s no way in Visual Studio to tell it not to build every target framework.

Conditional Target Frameworks

To get around the limitations of Visual Studio we’ve introduced some conditional logic into the project files that determine which TFMs are built. If we look at the top of the MvvmCross.csproj file we can see that there are a number of TFM lists.

<Project Sdk="MSBuild.Sdk.Extras">
  <PropertyGroup Condition=" '$(TargetsToBuild)' == 'All' ">
    <TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">netstandard2.0;net461;Xamarin.iOS10;Xamarin.Mac20;Xamarin.TVOS10;Xamarin.WatchOS10;MonoAndroid90;tizen40;netcoreapp2.1;uap10.0.16299</TargetFrameworks>
    <TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netstandard2.0;net461;Xamarin.iOS10;Xamarin.Mac20;Xamarin.TVOS10;Xamarin.WatchOS10;MonoAndroid90;tizen40;netcoreapp2.1</TargetFrameworks>
  </PropertyGroup>

  <PropertyGroup Condition=" '$(TargetsToBuild)' != 'All' ">
    <TargetFrameworks Condition=" '$(TargetsToBuild)' == 'Android' ">netstandard2.0;MonoAndroid90;</TargetFrameworks>
    <TargetFrameworks Condition=" '$(TargetsToBuild)' == 'Uap' ">netstandard2.0;uap10.0.16299</TargetFrameworks>
    <TargetFrameworks Condition=" '$(TargetsToBuild)' == 'iOS' ">netstandard2.0;Xamarin.iOS10</TargetFrameworks>
  </PropertyGroup>

In the Directory.build.props file for MvvmCross, which we’ll come to in a minute, we’ve defined a property called TargetsToBuild. If this is set to ‘All’, we set the TargetFrameworks property to include all the TFMs (except when the OS isn’t Windows_NT where we leave out Uap). However, if the TargetsToBuild is something different we restrict the TargetFrameworks property to the appropriate TFM (eg MonoAndroid90 when the TargetsToBuild is ‘Android’). We also include netstandard2.0 to help ensure developers don’t accidentally include platform specific code in files that are used by all platforms.

I mentioned that we define the TargetsToBuild in the Directory.build.props file. Currently what you’ll find in the Directory.build.props file in the MvvmCross repository is something that looks like the following:

<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
  <TargetsToBuild>All</TargetsToBuild> 
  <!--<TargetsToBuild>Android</TargetsToBuild>-->
  <!--<TargetsToBuild>Uap</TargetsToBuild>--> 
  <!--<TargetsToBuild>iOS</TargetsToBuild>--> 
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' != 'Debug' ">
  <TargetsToBuild>All</TargetsToBuild>
</PropertyGroup>

The first PropertyGroup, which is used when the build Configuration is set to Debug, allows the developer to switch TargetsToBuild by simply commenting and uncommenting the different values. For example to switch to Android I simply comment out the line with ‘All’ in it and uncomment the ‘Android’ line. Note that you have to restart Visual Studio for this change to take effect.

Solution Filters (.slnf)

Using the TargetsToBuild property works really well and significantly cuts down on the build time when doing development with MvvmCross. However, whenever I step in to work on MvvmCross I find it frustrating that Visual Studio has to load all 50+ projects when I’m not going to use them all. I’ve noticed recently that a couple of OSS projects such as Platform.Uno and Allan Richie’s Shiny have started to include solution filter files (.slnf). Solution Filter files make it easy to open a solution with only a subset of projects loaded.

There’s plenty of documentation on how to create a solution filter file, so I’m not going to cover that here. However, for MvvmCross it makes sense to have different solution filters for work with each platform. In my PR I’ve created filters for All, Android, iOS and Uap, which match the different TargetsToBuild options (the reason for this will become evident shortly).

Launch Automation

I was looking around for a way to combine solution filtering with the conditional logic for TFMs. Unfortunately there’s currently no mechanism for linking them. What this means is that developers wanting to work for particular platform have to first set the TargetsToBuild in the Directory.build.props, and then open the solution using the appropriate filter file.

I figured that if I can’t link the solution filter with a specific list of TFMs, the least I could do was to automate the process of setting the TargetsToBuild and launching the solution. For this I created a series of .bat files (yeh, old school I know): LaunchVS.All.bat, LaunchVS.Android.bat, LaunchVS.iOS.bat and LaunchVS.Uap.bat. Each of these platform specific launch files invokes “LaunchVS.bat XXX” where XXX is the corresponding platform (eg “LaunchVS.bat Android”).

The LaunchVS batch file does two things:

  • Invokes a powershell command that does a find and replace to update a property, TargetsToBuildDeveloperOverride, in the Directory.build.props file.
  • Launches Visual Studio by starting the corresponding solution filter file. Hence the reason why the names of the filter files needed to match the platforms options for TargetsToBuild.
powershell -Command "(gc Directory.build.props) -replace '<TargetsToBuildDeveloperOverride>[a-zA-Z]*</TargetsToBuildDeveloperOverride>', '<TargetsToBuildDeveloperOverride>%~1</TargetsToBuildDeveloperOverride> ' | Out-File -encoding ASCII Directory.build.props"
start MvvmCross.%~1.slnf

At this point you might be wondering why we’re updating the TargetsToBuildDeveloperOverride property instead of TargetsToBuild. The reason for this is that if we simply did a find-and-replace using the TargetsToBuild property it would replace both the value used for the Debug configuration but also the other configurations. It’s important that we don’t modify the Release configuration by accidentally changing the TFMs, since this could then accidentally be committed to the repository. To avoid this problem I created the TargetsToBuildDeveloperOverride property which is only used to set the TargetsToBuild property for the Debug configuration.

<PropertyGroup>
  <TargetsToBuildDeveloperOverride>Uap</TargetsToBuildDeveloperOverride>          
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
  <TargetsToBuild>$(TargetsToBuildDeveloperOverride)</TargetsToBuild> 
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' != 'Debug' ">
  <TargetsToBuild>All</TargetsToBuild>
</PropertyGroup>

With these changes, all a developer has to do when working with a particular platform is to double-click the appropriate .bat file. The Directory.build.props will be updated and the corresponding .slnf file will be launched. If the updated Directory.build.props does accidentally get committed to the MvvmCross repository it won’t affect the Release build configuration. The LaunchVS batch file is clever enough to switch from one platform to another without the developer having to undo changes to the Directory.build.props file.

Hopefully in this post you’ve seen one option for switching TFMs and making it easier for developers to work with a multi-targeted solution. If you use multi-targeting in your application or library, it’s work considering adding filtering and conditional TFMs to make development easier.