This is exactly what MvvmCross, ReactiveUI and other frameworks do. So claiming no one in the world has done this is some kind of wild imagination.
One way to do it is to create a Multi-Target library, just be aware that Visual Studio for Mac doesn't like those projects yet and you will have issues creating and developing such a library there. However, if you only develop on Windows Visual Studio 2017 and newer supports these kind of projects very well.
You can start off by creating a .NET Standard library in VS, which will give you a project with a single .cs file containing Class1, go a head and remove that.
If you edit the .csproj file you will see a PropertyGroup
in the top of the file containing a property called TargetFramework
. Change that to TargetFrameworks
and list all the desired frameworks you want to target:
<PropertyGroup
<TargetFrameworks>netstandard2.0;Xamarin.iOS10;MonoAndroid81;uap10.0.16299</TargetFrameworks>
If you want easy support in Multi-Target libraries, I advise you to switch to MSBuildSDKExtras for your project SDK, as it will handle a lot of the quirks that comes with targeting Xamarin platforms. It will add necessary properties to your project to make all of this work.
So first line of the .csproj file will have to look like:
<Project Sdk="MSBuild.Sdk.Extras/1.6.68">
Now you need to specify in which folders your platform specific code is in. What I usually do is to create a platforms
folder with android
, ios
and uap
folders and consider the rest of the code platform independent and residing in common
.
<ItemGroup>
<Compile Remove="platforms***.cs" />
<None Include="platforms***.cs" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('netstandard')) ">
<Compile Include="platformscommon***.cs" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('Xamarin.iOS')) ">
<Compile Include="platformsios***.cs" />
<Compile Include="platformscommon***.cs" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('MonoAndroid')) ">
<Compile Include="platformsandroid***.cs" />
<Compile Include="platformscommon***.cs" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('uap')) ">
<Compile Include="platformsuap***.cs" />
<Compile Include="platformscommon***.cs" />
</ItemGroup>
To nicely create NuGet packages from this Multi-Target library, I usually add a Directory.build.props
file in the root of the solution where I specify properties for the Packages:
<Project>
<PropertyGroup>
<Company>Cheesebaron</Company>
<Copyright>Copyright ? Cheesebaron</Copyright>
<RepositoryUrl>https://github.com/cheeesebaron/cool-stuff</RepositoryUrl>
<Authors>Cheesebaron</Authors>
<Owners>Cheesebaron</Owners>
<PackageReleaseNotes />
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<RepositoryType>git</RepositoryType>
<Product>$(AssemblyName) ($(TargetFramework))</Product>
<NeutralLanguage>en</NeutralLanguage>
<LangVersion>latest</LangVersion>
<NoWarn>$(NoWarn);1591;1701;1702;1705;VSX1000;NU1603</NoWarn>
<GenerateDocumentationFile Condition=" '$(Configuration)' == 'Release' ">true</GenerateDocumentationFile>
<GeneratePackageOnBuild Condition=" '$(Configuration)' == 'Release' and '$(IsTestProject)' != 'true'">true</GeneratePackageOnBuild>
<Platform>AnyCPU</Platform>
<DebugType>portable</DebugType>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IsTestProject>$(MSBuildProjectName.Contains('UnitTests'))</IsTestProject>
</PropertyGroup>
</Project>
Also I add a Directory.build.targets
file defining the constants for the different platforms:
<Project>
<PropertyGroup Condition="$(TargetFramework.StartsWith('netstandard'))">
<DefineConstants>$(DefineConstants);NETSTANDARD;PORTABLE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework.StartsWith('Xamarin.iOS'))">
<DefineConstants>$(DefineConstants);MONO;UIKIT;COCOA;IOS</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework.StartsWith('MonoAndroid'))">
<DefineConstants>$(DefineConstants);MONO;ANDROID</DefineConstants>
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
<AndroidResgenClass>Resource</AndroidResgenClass>
<AndroidResgenFile>ResourcesResource.designer.cs</AndroidResgenFile>
</PropertyGroup>
</Project>
If you are targeting Android, you will probably also need to add:
<None Include="Resources*.cs" />
<Compile Remove="Resources*.cs" />
To the first ItemGroup
in your .csproj file and add something like:
<AndroidResource Include="Resources***.xml" SubType="Designer" Generator="MSBuild:UpdateAndroidResources" />
<AndroidResource Include="Resources***.axml" SubType="Designer" Generator="MSBuild:UpdateAndroidResources" />
To the Android ItemGroup
to properly update Android Resources.
Adding files in Visual Studio can be a bit tricky and it will often add them in the wrong place for the first couple of files until you have something in all of the platform specific folders. But you shouldn't need anything else in the .csproj file as long as the files are placed correctly in the structure we've defined.
You can see examples of Multi-Target libraries here:
https://github.com/MvvmCross/MvvmCross/blob/develop/MvvmCross/MvvmCross.csproj
https://github.com/MvvmCross/MvvmCross/blob/develop/Directory.build.props
https://github.com/MvvmCross/MvvmCross/blob/develop/Directory.build.targets
And similarly for ReactiveUI:
https://github.com/reactiveui/ReactiveUI/blob/master/src/ReactiveUI/ReactiveUI.csproj
As of now it is not super easy to get started, but is fine to work with.
Alternatives to this are .NET Standard libraries and platform specific libraries adding the Bait-and-switch pattern.