(Ba ba… ba bump) From the day I was born
(Ba ba… ba bump) I was writing cross platform apps utilizing Shared Projects code
(Ba ba… ba bump) Then I started adding platform specific features
(Ba ba… ba bump) And my code became a mess of hash codes and everything got lost…
(Ba ba… ba bump) I’ve got the cross platform preprocessor directive blues!!!
Am I the only one who sings the blues when dealing with shared projects and all the preprocessor directives they bring? Shared projects are a great way to share a single code base across platforms, but start to introduce a lot of platform specific functionality into them – and watch out!
Preprocessor Directive Blues!
Using shared projects doesn’t necessarily mean we will end up with a mess of hash signs and #if statements and generally an un-followable mis-mash of code – there are techniques we can use to refactor away the preprocessor directives, pull ourselves out of the blues and find some cheer with shared projects!
Shared Projects Recap
Before we let the sunshine in, let’s recap a bit on why I’d even consider using shared projects in the first place if they have the potential to cause me to sing the blues!
Shared projects are a way to share a single code base across platforms (in our case – iOS, Android, Windows Phone) all within the same solution. The same code. The same functionality. Different platforms.
Shared projects also provide full access to the .Net Framework, unlike another means to achieve cross platform code reuse – PCLs. (Plus a dirty secret that people don’t like to mention often … shared projects are easier & faster to get up and running than PCLs – there’s not as much up front design work to them).
For a fuller primer on shared projects, see my previous post here.
Shared projects are compiled directly into the referencing project’s binary (they do not emit a DLL). This means that if we need to do anything platform specific within them (and there are times we do, like accessing the file system) that we have to use preprocessor directives to delineate that platform specific functionality.
We end up with code looking like this:
#if __IOS__
// Open the documents folder and write to the file
var docPath = MonoTouch.Foundation.NSFileManager.DefaultManager.GetUrls(
MonoTouch.Foundation.NSSearchPathDirectory.DocumentDirectory,
MonoTouch.Foundation.NSSearchPathDomain.User)[0];
fileName = Path.Combine(docPath.Path, "data.csv");
#elif __ANDROID__
fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),"data.csv");
#endif
Which isn’t bad until those preprocessor directives start getting spread around everywhere, which just makes for hard to read code. That in itself is enough to get me humming the blues.
But what will get me singing – howling – the blues is the fact that the functionality contained within those preprocessor directives has a sneaky way of getting spread around everywhere in the project and there’s not a good place to go look to see all of the iOS specific code or the Android specific code at one time. You have to look for all of the #if statements, check to see if they’re specifically calling out the target operating system, and then finally look at the functionality you were checking on in the first place.
Ugh.
I’ve got the preprocessor directive blues!!
Let The Sunshine In!
Luckily we don’t have to sing the blues for the rest of our days. There are ways, and one in particular, which can make the clouds part and let the sun shine down on shared projects. We can have all the benefits of shared projects – common code across platforms… full access to the .Net Framework… the ability to get up and running quicker than with PCLs (shh!)… platform specific code… all without having to resort to preprocessor directives!
How is this possible?
Partial Classes
Partial classes?!? You mean that thing from back in the web form days of ASP.Net?
Yup.
Of course partial classes are used far more often than just in web forms – they’re used anytime there’s generated code that may need to be extended by a developer. (In fact, take a look at the iOS view controller classes that Xamarin produces – all partial classes).
So how does that help us here?
Since our goal is to remove all of the preprocessor directives from the shared project classes:
- The preprocessor directives are used to delineate platform specific code.
- Partial classes allow us to implement a single class in two different files.
- We can then implement the class that contains the preprocessor directive as a partial class in the shared project and in the platform project.
- Then we can move all of our platform specific code out of the preprocessor directives in the shared project’s partial class and into the partial class inside of the platform project.
- There we have it, shared projects – platform specific code – no preprocessor directives!
I’ll admit – this isn’t anything advanced – but it is simple and elegant in its own way. My tune is changing already!
To add to it, there’s another concept called Partial Methods. This allows you to create a method declaration in one partial class file and provide its body in another. If no body is provided, then the compiler strips the call to that method out. In other words, it’s like having a preprocessor directive with an #if – but no #else.
Refactoring To Partial Classes
Let’s rid ourselves of the blues once and for all and refactor a solution which has both an iOS and Android project in it, both of which use the same shared project to have them use partial classes instead.
We’re going to work with the same project as we did in the original cross platform code sharing – shared project post. There we created an app to track Tabatas, or workout and rest intervals.
Preprocessor directives were used in this project to access the file system in a platform specific way to determine the full path of where to store a file to. That’s what we want to move out into partial classes, thus removing the preprocessor directives.
You can find the code for the original and refactored project on GitHub. The refactored project is just a branch of the original (I find it easier just to flip between branches when viewing the same code, just refactored). The original project, with preprocessor directives is here, on the master branch. The partial class one is here, on the AdvancedShared branch.
The first thing I want to do with the new app is rename the shared class Tabata to TabataWorkout to illustrate how refactoring is problematic with shared projects. As illustrated below, the Android project was the start up project at the time of the refactoring, Xamarin Studio only found the following classes to change – all Android – none of the iOS ones.
This will still be problematic when partial classes are introduced as well. But we’re on the happy path – let’s not let that bring us down!
The next step in the overall change to partial classes is to change the definition of the TabataWorkout class from public class TabataWorkout
to public partial class TabataWorkout
. Then of course we need to create a new class in each of the platform projects with the same definition. One thing to watch out for – make sure the namespace is the same between the class in the shared project and in the platform projects!
One thing I wanted to do, which is not necessary, was to create an interface, so I can be sure all of my partial classes followed the same definition. This is just a nicety in this case. Any class in the shared project which needs to access the file system to get the file name implements the following:
public interface ITabataFileInfo
{
string GetFileName();
}
To finish off the implementation of the partial class, it’s as easy as implementing the GetFileName
function in each of the platform specific partial classes.
For Android we get:
public partial class TabataWorkout
{
public string GetFileName()
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),"data.csv");
}
}
And on iOS we get:
public partial class TabataWorkout
{
public string GetFileName()
{
var docPath = NSFileManager.DefaultManager.GetUrls(
NSSearchPathDirectory.DocumentDirectory,
NSSearchPathDomain.User)[0];
return Path.Combine(docPath.Path, "data.csv");
}
}
Then the code in our shared project goes from:
#if __IOS__
// Open the documents folder and write to the file
var docPath = MonoTouch.Foundation.NSFileManager.DefaultManager.GetUrls(
MonoTouch.Foundation.NSSearchPathDirectory.DocumentDirectory,
MonoTouch.Foundation.NSSearchPathDomain.User)[0];
fileName = Path.Combine(docPath.Path, "data.csv");
#elif __ANDROID__
fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),"data.csv");
#endif
To:
string fileName = this.GetFileName ();
Wow, that’s much better! And we can see all of the platform specific code in one spot instead of having to wade through a sea of preprocessor directives! (Plus you can use “Go To Declaration” as you usually would to jump to the declaration of the function in the active platform project).
Let’s briefly touch on partial methods. As I mentioned above, a partial method is declared in one partial class file with the keyword partial
and implemented in another.
For example:
partial void BuzzPhone();
partial void BuzzPhone()
{
SystemSound.Vibrate.PlayAlertSound();
}
Because calls to partial methods are ignored by the compiler if the method body is not supplied, we can have functionality targeted to a specific platforms and not others without having to introduce more “if” statements.
Summary
All it took was partial classes to bring us from singing the blues with a mess of preprocessor statements, not knowing where any particular code was at any particular time, all the way to smiling and relaxing – with nice clean code!
Shared projects are powerful. They provide a means to accomplish cross platform code sharing by allowing a common code base to be compiled directly into the referencing project. It’s that fact that allows shared projects to have access to the full .Net framework and gets us up and running on them very quickly.
However, anytime we need to have platform specific code in a shared project, a mess of preprocessor statements can result. And it can go from a mess to a disaster in no time at all.
Partial classes are a way to clean all of that up. They allow you to keep the power of shared projects with the common code in one place – but to move all of the platform specific code out of preprocessor directives in the shared project and into partial classes in the platform project.
That’s music to my ears!
Comments