Missing Files in Multi-Targeted Project

In the .NET ecosystem there have been a number of different approaches to building libraries that work cross platform (i.e. support building apps for different platforms such as iOS, Android, Windows etc). One of the more recent is to build a multi-targeted project that can light up either different features or just different implementations for each supported platform. In this post we’re going to look at a common issue developers face with multi-targeted projects where their files seem to disappear.

Let’s start with a new Xamarin.Forms application – the Visual Studio template for a new Mobile App (Xamarin.Forms) includes head projects for iOS, Android and Windows and an SDK style project that initially targets .NET Standard 2.0, which initially holds the XAML files. For the purposes of this post I’m just going to focus on Android and Windows but the concept works just as well for iOS. With this in mind I’ve removed the iOS head project and we’re going to jump into the project file for the .NET Standard project.

We’ll start by changing the Sdk attribute to use MSBuild.Sdk.Extras and in this case we’re going to specify the version inline (I would normally recommend using a global.json file to define the version across all projects in a solution). We’re also going to replace the TargetFramework element with TargetFrameworks, specifying Android and UWP.

<Project Sdk="MSBuild.Sdk.Extras/2.1.2">
  <PropertyGroup>
	  <TargetFrameworks>monoandroid10.0;uap10.0.16299</TargetFrameworks>
	  <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
  </PropertyGroup>

Note: You’ll notice that I’ve removed netstandard2.0 as a target – this was intentional but we’ll leave the discussion of this for a future post. The upshot is that with multi-targeting there’s no benefit to having the .NET Standard target and with the current woeful support for multi-targeted projects in Visual Studio, removing this target helps reduce build times.

The next thing we’re going to do is add some platform specific code for both Android and Windows. In this case it’s a simple class that has a single static property, WelcomeText, that returns a platform specific string. Here’s the Windows version.

public static class Literals
{
    public static string WelcomeText => "Hello from Windows";
}

We’ll create two instances of this class, placing them within the /Platforms/Android and /Platforms/Windows folders (changing the WelcomeText accordingly). Note that we would normally keep the namespace in sync with the folder structure. However, in this case it’s important that both copies of the Literals class belong to the same namespace.

After adding the Literals class (both instances) you’ll see a compile error indicating that there are multiple definitions for the Literals class.

Error CS0101 The namespace 'MultiTargetFileSample' already contains a definition for 'Literals' MultiTargetFileSample (uap10.0.16299) C:\temp\MultiTargetFileSample\MultiTargetFileSample\MultiTargetFileSample\Platforms\Windows\Literals.cs 7 Active

To resolve this, we first need to exclude the entire Platforms folder and then add back the folder for the specific platform that’s being built. To do this we need to define some ItemGroup elements in the csproj.

<ItemGroup>
	<Compile Remove="Platforms\**\*.cs" />
</ItemGroup>

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

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

We’re back to being able to compile the application, and if you reference the Literals.WelcomeText property in the application you’ll see it return different values based on which target platform you’re running.

However, we’ve introduced a bit of an issue, shown in the following image. I’ve toggled “Show all files” (fourth icon from the right in the Solution Explorer toolbar shown in the image) so that you can see the Literals.cs sitting in both the Android and Windows folders. The issue is that the Literals.cs that sits under the Windows folder isn’t included – this means Visual Studio doesn’t know about the file, effectively excluding it from things like search.

You might think that simply switching the startup project would be enough to tell Visual Studio that it should show the files for the specified target platform. Unfortunately this is not the case – if you look again at the above image you’ll see that the startup project is already set to the Windows project (the bolded MultiTargetFileSample.UWP project), yet only the Android files are being included.

It turns out that Visual Studio doesn’t handle multi-targeted projects very well and will only show the included files for the first target framework in the TargetFrameworks list. If you change the order to “uap10.0.16299;monoandroid10.0” you’ll see that the Windows files are shown as being incuded.

Switching the order of the target frameworks, whilst quite effective, is a bit of a hassle to do. Luckily there is one way to improve the situation, which is to reinclude all the files under the Platforms folder but with a build action of None, instead of Compile.

<ItemGroup>
	<Compile Remove="Platforms\**\*.cs" />
	<None Include="Platforms\**\*.cs" />
</ItemGroup>

Now you’ll see all the files under the Platforms folder being included.

Note that if you click on a file and look at the Properties, you’ll see that some files have a build action of None, whilst others have it set to Compile. Ignore this issue as it’s an issue with Visual Studio’s handling of multi-targeted and won’t affect the compilation of the code.

Hopefully this will help solve just one of the frustrations with dealing with multi-targeted projects.

Leave a comment