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.

Leave a comment