Click here to Skip to main content
15,867,943 members
Articles / Programming Languages / C#
Article

Portable Class Libraries: Papering Over Platform Differences

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
2 Jan 2014CPOL7 min read 25.1K   212   10   4
Portable Class Libraries (PCLs) can only call other PCLs. There is a way to implement PCLs that "call" platform specific services by writing a PCL and platform specific equivalent libraries.

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:

C#
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:

C#
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:

C#
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".

Using the Add Existing Item dialog and selecting 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.

Using the Add Existing Item dialog and selecting Add As Link

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.

Use build tab of project properties to define FILE_SYSTEM

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:

C#
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:

C#
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:

C#
[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).

XML
<?xml version="1.0" encoding="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 ifdefs. 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Other University of Washington
United States United States
I'm a professor of physics at the University of Washington - my field of research is particle physics. I went into this because of the intersection of physics, hardware, and computers. I've written large experiment data aquisition systems (I've done a lot of multi-thread programming). My hobby is writing tools and other things that tend to be off-shoots of work-related projects.

Comments and Discussions

 
GeneralVery Helpful!!! Pin
Viv Rajkumar29-Jan-14 1:03
Viv Rajkumar29-Jan-14 1:03 
QuestionVery good article! Pin
Volynsky Alex4-Jan-14 23:43
professionalVolynsky Alex4-Jan-14 23:43 
AnswerRe: Very good article! Pin
gordonwatts11-Jan-14 6:47
gordonwatts11-Jan-14 6:47 
GeneralRe: Very good article! Pin
Volynsky Alex11-Jan-14 7:06
professionalVolynsky Alex11-Jan-14 7:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.