The Bait and Switch PCL Trick
Some folks on Twitter were debating about how underpowered Portable Libaries are and how shared code is better. While I'm not going to get into this debate (pro-tip, they're wrong :P), not knowing how to "proxy" platform-specific features can lead to a lot of frustration.
Daniel Plaisted has written about one way to expose platform-specific features that works well, but he also taught me a more clever trick once while I was working on Splat.
It took me awhile to understand it fully, but it completely changed my approach to building Portable Libraries and made me realize I was totally Doing It Wrong™. Glenn Block has taken to calling this the "Bait and Switch PCL", whereas Miguel de Icaza has called this the "Advanced PCL" pattern. I'll call it "Bait and Switch" because it's more descriptive :)
If you can get away with it, you don't need this trick
If you can write your library within the constraints of what the Portable Library profiles provides, awesome! You don't need this trick at all. This is definitely a more advanced approach, because you'll need to create platform-specific csproj files for every platform you support. This is a bit of a pain if you're not going to take advantage of it.
Alternate approaches / mistakes
One mistake I made initially while creating Portable Libraries is to think of the Portable version of the library as its own entity - you can see this (mistaken) pattern in the older version of Akavache, where I had a Akavache.Portable
NuGet package and an Akavache
platform library. This is how most people conceptualize Portable Libraries (past myself included!)
This is the wrong way to do things, don't do it that way!
However, to understand the right way, we need to put together a few disparate facts.
There is no such thing as a portable app
One thing that's important to understand from a philosophical perspective is, the platform that a PCL profile defines doesn't actually exist. No app will ever run under this set of libraries - it's always running under a profile with more features.
NuGet realizes that this is the case, because it will always prefer a platform library to a PCL. This aspect of NuGet is critical to how the Bait and Switch works.
If you take this to its logical conclusion, you realize that, if you provide an Assembly for every platform in the PCL, this means that nobody will ever execute the code in the PCL version, only the platform versions.
We're going to define a PCL that does nothing
Let's implement Splat's BitmapLoader
as a Bait and Switch PCL. First, we'll define a class called BitmapLoader (for brevity, I'll only implement Load
). First, the return type, which we'll define in both the PCL and the Platform library:
public interface IBitmap
{
public float Width { get; }
public float Height { get; }
}
Now, let's define our bitmap loader in the PCL:
public static class BitmapLoader
{
public Task<IBitmap> LoadImage(Stream source)
{
return default(Task<IBitmap>);
}
}
Wait, that's the worst bitmap loader ever. It doesn't even do anything! But, what if the platform-specific version didn't do the same thing? Let's write a UIKit-based one:
public static class BitmapLoader
{
public Task<IBitmap> LoadImage(Stream source)
{
var data = NSData.FromStream(sourceStream);
var tcs = new TaskCompletionSource<IBitmap>();
UIApplication.SharedApplication.InvokeOnMainThread(() => {
try {
tcs.TrySetResult(new CocoaBitmap(UIImage.LoadFromData(data)));
} catch (Exception ex) {
tcs.TrySetException(ex);
}
});
return tcs.Task;
}
}
Okay, so how does that help
Remember that fact I told you about NuGet? If we wrote versions for every platform, then made a NuGet package for this library, it might look something like this:
BitmapLoader.nuspec
lib/
Portable-Net45+NetCore45+MonoTouch+MonoAndroid/
BitmapLoader.dll // PCL Version
Net45/
BitmapLoader.dll // WPF Bitmap Loader
MonoTouch/
BitmapLoader.dll // UIKit Bitmap Loader
MonoAndroid/
BitmapLoader.dll // Android Bitmap Loader
NetCore45/
BitmapLoader.dll // WinRT Bitmap Loader
We've covered every platform that the PCL supports, so that means nobody will be using the Portable version (remember what NuGet does from earlier!), except for other portable libraries. Which means, that no real executable will ever run the code in the Portable BitmapLoader.
What even is a method invocation?
But how can we switch out an assembly we referenced for a completely different one?! Doesn't that break? To understand why that doesn't happen, we need to understand what a method invocation really is. Take a look at this monodis
output from a random method I picked from Splat:
// method line 40
.method public static hidebysig
default valuetype [System.Drawing]System.Drawing.PointF ScaledBy (valuetype [System.Drawing]System.Drawing.PointF This, float32 factor) cil managed
{
.custom instance void class [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute::'.ctor'() = (01 00 00 00 )
// Method begins at RVA 0x28f5
// Code size 24 (0x18)
.maxstack 8
IL_0000: ldarga.s 0
IL_0002: call instance float32 valuetype [System.Drawing]System.Drawing.PointF::get_X()
IL_0007: ldarg.1
IL_0008: mul
IL_0009: ldarga.s 0
IL_000b: call instance float32 valuetype [System.Drawing]System.Drawing.PointF::get_Y()
IL_0010: ldarg.1
IL_0011: mul
IL_0012: newobj instance void valuetype [System.Drawing]System.Drawing.PointF::'.ctor'(float32, float32)
IL_0017: ret
} // end of method PointMathExtensions::ScaledBy
Don't worry if this makes zero sense. The important part is, at IL_0002
we see a call
instruction - while this output is prettified a bit, we can see, that every time we call a method, it's encoding the following information:
FullyQualifiedAssemblyName + Fully Qualified Class Name + Method
This means, as long as the platform binary matches on assembly name, version, and class structure, we can replace it with the platform binary.
So, that means, your PCL binary and platform binary both have to be called Foo
, not Foo.PCL
and Foo.Platform
or something.
What have we learned
Here's a few takeaways that you can pull from this post that weren't actually found in the post because lazy:
- If you're referencing assemblies by-hand, always prefer the platform one over the PCL
- Portable Class Libraries can do way more than they initially appear, if you think about them the Right Way™
- Platform libraries make for way less
#ifdefs
and more understandable code because we can just have separate files that implement the same class (i.e.WinRTBitmapLoader.cs
,WP8BitmapLoader.cs
, etc etc) - You have to define all of the types in all of the libraries - don't define types in just the PCL, because the app won't actually reference that PCL. Define all of the types everywhere, even if the PCL version has a dummy implementation.
Previous post: Using ReactiveCommand
Next post: Creating ViewModels with ReactiveObject