Introduction
I love Portable Class Libraries (PCLs). At least, I love the promise of them: write once, run everywhere. However, in practice, especially in these early days, things are not so rosy.
The basic problem: PCL's are viral. A PCL can call only another PCL. If you have a low-level service that is platform dependent you have to bend over backwards to call it in a PCL.
I find coding PCLs amazingly frustrating as a result.
It turns out you can make platform specific calls from a PCL. You just have to be sneaky. This article describes how to do this.
Background
One approach to solving the problem of calling platform specific code in a PCL is the interface approach. In this approach you create a library that implements
an interface that papers over the difference between platforms. For example, here is a simple interface that is supposed to return the name of the platform that it is called on:
public interface IBaseFunctionality
{
string PlatformString { get; }
}
First, I create a PCL that contains this interface. The client PCL, the PCL that needs to use this base functionality, can then reference this interface assembly.
Finally, in my Windows Store app I create an object that properly implements the Windows Store version of the IBaseFunctionality
, and pass that in to my client PCL.
While this works, this is a bit painful. Especially if you have lots of small units of functionality that are individual libraries. Worse, I generally discover
I'm missing these bits of platform-specific functionality late in the game. I then have to either pass in a global variable with the interface, or I have to pass the new
interface all the way down the call chain. I believe there is a better way.
I did not invent this method. I first saw it in the PCL Storage library. This library implements basic local storage
in Windows Phone, Windows Store, Android, and iOS. And yet you don't have to pass in any platform specific implementation. In the middle of your PCL code you just write:
IFolder rootFolder = FileSystem.Current.LocalStorage;
No matter what platform you are on, you can now write to the local storage. How cool is that?
Implementation is straight forward, if a bit tedious. In a nutshell you create a dummy PCL that implements the actual calls (like the
FileSystem.Current
above). You then
create platform specific versions that will implement the functionality. Then client PCL's will reference the dummy PCL, and your apps will reference the platform-specific version of the PCL.
Creating the PCL interface
The source code attached to this article contains code for the libraries as well as MSTest projects for Windows Phone 8, Windows Store, and the desktop. In this article
I assume a basic level of knowledge - you know how to create a PCL, for example. The code uses Visual Studio 2013, though I see no reason why these techniques can't be used in earlier versions.
The first step is to create the interfaces that are to paper over the platform specific implementations. We'll implement the IBaseFunctionality
interface so
that its PlatformString
property returns "Desktop", "Windows Store", or "Windows Phone" depending on which platform it is being used on.
Create a PCL library called PCLTestInterfaceLibrary, and add the IBaseFunctionality
interface to it. This library should contain all of the interfaces
that will paper over the differences between platforms.
Second create the dummy PCL library. For the sample I've called it PCLTestLibrary. In my case it returns a reference to the IBaseFunctionality
object:
public IBaseFunctionality FetchPlatformUnique
{
get
{
var r = CreatePlatformObject();
if (r == null)
throw new NotImplementedException(
"The platform version of the PCLTest library was not linked in!");
return r;
}
}
private static IBaseFunctionality CreatePlatformObject()
{
#if SILVERLIGHT
return new PCLTestPhoneLibrary.PhoneBaseFunctionality();
#elif NETFX_CORE
return new WinRTBaseFunctionality();
#elif FILE_SYSTEM
return new PCLTestDesktopLibrary.DesktopBaseFunctionality();
#else
return null;
#endif
}
I put this in a class called PlatformFetcher
.
There are a few things to note about this code. The first thing to note is the CreatePlatformObject
object. Note the #if'd code. We'll get to that
when we implement a platform specific library. But for now, in this PCL, none of the preprocessor macros (SILVERLIGHT, NETFX_CODE, or FILE_SYSTEM) are defined.
In short, if this PCL was actually used by an app, they would get a null return from CreatePlatformObject
.
Second, the actual creation of the object is done in CreatePlatformObject
. If a null is returned, then an exception is thrown. This is strictly a programmer
user-interface issue. This code should never be executed - a platform specific version of it should be. If the programmer forgets to reference one of the platform specific
libraries, then they they should get an error that indicates in some friendly way they have forgotten to include the platform specific library!
Creating Platform Specific Versions of the PCL
Let's create the Windows Desktop version of the library. The others are straight forward once you understand this.
Create a Windows Class Library can call it PCLTestDesktopLibrary. Add a reference to the PCLTestInterfaceLibrary assembly (but only that assembly!).
In it link to the PlatformFetcher
class you created in the PCL.
It is very important to link to this file in the platform specific version of the library! To do this, first right click on the PCLTestDesktopLibrary project,
and select Add, Existing Item... Select the PlatformFetcher file from the PCLTestLibrary project. Before clicking Add, however,
click the small downward pointing triangle and select "Add as link".
You must also make sure that this project generates an assembly with the same name as the dummy PCL assembly we made earlier, PCLTestLibrary
.
Edit the project property pages, and alter the assembly name.
There is one special case for the desktop library. There is no uniquely defined preprocessor symbol that will select out our specific line. So we need to define one.
Following the lead of the PCL Storage library, I choose FILE_SYSTEM
, but it is arbitrary. It is set in the project properties dialog box's Build tab.
Now that is done, we can actually create the class that will do the work. Add a new class to the platform project that contains the platform specific code:
using PCLTestInterfaceLibrary;
namespace PCLTestDesktopLibrary
{
class DesktopBaseFunctionality : IBaseFunctionality
{
public string PlatformString
{
get { return "Desktop"; }
}
}
}
Obviously, this is where you get to implement the platform specific behaviour of the
IBaseFunctionality
object. This done, the project should compile correctly.
Using the PCL from another PCL library
In the PCL that will use this new platform specific low level code, you need only reference two assemblies: the Interface and the dummy PCL.
I created a ClientPCLLibrary PCL project. From it I referenced the PCLTestLibrary and PCLTestInterfaceLibrary projects.
I then added the following code:
namespace ClientPCLLibrary
{
public class DoItNow
{
public static string GetThePlatform()
{
var tmp = new PCLTestLibrary.PlatformFetcher().FetchPlatformUnique;
return string.Format("Returned platform string is {0}.", tmp.PlatformString);
}
}
}
This should compile just fine.
Using the PCL in an application
The final step is to reference your ClientPCLLibrary in your platform specific app. In the sample project included with this post I just used Unit Test projects.
For the Windows Store test app I created a Windows Store Unit Test app. This is a platform specific project!
Next, I added the PCLTestInterfaceLibrary
, PCLTestWindowsLibrary
, and ClientPLCLibrary
projects as references.
Note that the dummy PLC project, PLCTestLibrary
is no where to be found!
And finally I wrote a small bit of unit test code:
[TestClass]
public class PlatformUtil
{
[TestMethod]
public void TestFetch()
{
Assert.AreEqual("Returned platform string is Windows RT.", DoItNow.GetThePlatform());
}
}
Which, obviously, worked!
Nuget
Nuget is now the way to distribute libraries. How does the above fit with Nuget? Modern Nuget spec files have enough flexability to support this out of the box. In short,
you can make a nuget package that will correctly include all files.
Though I've not inserted this dummy library on NuGet, I did make a nuspec file which correctly builds the package for distribution, which is included in the example code.
The key thing to keep in mind is that a pair of files must be directed toward the PCL platform as well as each specific platform (the interface library,
and the PCL dummy library or the platform specific version).
="1.0" ="utf-8"
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata minClientVersion="2.7.2">
<id>PCLTest</id>
<version>1.0.0</version>
<title>PCLTest - PCL with platform dependent functionality</title>
<authors>Gordon Watts</authors>
<owners>Gordon Watts</owners>
<projectUrl>https://github.com/gordonwatts/PCLForFrameworkDependent</projectUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>A consistent API to determine what flavor of platform you are
running on across the Desktop, Windows Phone, and Windows Store apps.</description>
</metadata>
<files>
<file src="PCLTestLibrary\bin\Release\PCLTestLibrary.dll"
target="lib\portable-net45+wp8+win8\PCLTestLibrary.dll" />
<file src="PCLTestInterfaceLibrary\bin\Release\PCLTestInterfaceLibrary.dll"
target="lib\portable-net45+wp8+win8\PCLTestInterfaceLibrary.dll" />
<file src="PCLTestDesktopLibrary\bin\Release\PCLTestLibrary.dll"
target="lib\net45\PCLTestLibrary.dll" />
<file src="PCLTestInterfaceLibrary\bin\Release\PCLTestInterfaceLibrary.dll"
target="lib\net45\PCLTestInterfaceLibrary.dll" />
<file src="PCLTestWindowsLibrary\bin\Release\PCLTestLibrary.dll"
target="lib\win8\PCLTestLibrary.dll" />
<file src="PCLTestInterfaceLibrary\bin\Release\PCLTestInterfaceLibrary.dll"
target="lib\win8\PCLTestInterfaceLibrary.dll" />
<file src="PCLTestPhoneLibrary\bin\Release\PCLTestLibrary.dll"
target="lib\wp8\PCLTestLibrary.dll" />
<file src="PCLTestInterfaceLibrary\bin\Release\PCLTestInterfaceLibrary.dll"
target="lib\wp8\PCLTestInterfaceLibrary.dll" />
</files>
</package>
Comments
There are lots of variations on a theme here. For example, there is no need to implement interfaces. One could just have a few static methods in the dummy PCL.
I'd strongly suggest not putting the code directly into the common file with
ifdef
s. It makes it very hard to read. Instead, keep the project as it currently
is laid out - put your platform specific code in a separate file.
I definitely resent the tediousness that one has to go through. Someone with better macro skills than I can probably generate a template project that would make
the generation of this much simpler!
It should be noted that Visual Studio 2013 has a good deal of difficulty dealing with files that are linked to multiple projects (like our PlatformFetcher
file).
It will complain the file is open in another project, it will predict errors were there aren't any. After enough use you'll start to see patterns. But don't trust the error prediction
in the shared file; just build to see if the actual compiler complains!
I hope this was useful and inspires to you create small PCL libraries that make sense cross platform. That done, it will be a lot easier for all of us to write general libraries
and leverage each others code in our projects!
The code for this project can be found on github. I will update it as I find mistakes or extra functionality is required.