Xamarin Forms – Using partial methods for platform specific code in a Shared project type.

There has been a bit of a public debate between Miguel de Icaza, a co-founder and CTO of Xamarin, and Jason Smith, the lead engineer on the Xamarin.Forms project, about whether it is better to use PCLs (pronounced “pickles”) or Shared projects for the core shared code in Xamarin.Forms solutions (this debate could be about any cross platform solution, not just Xamarin.Forms).

Miguel argues in this blog post that a “PCL is just too cumbersome for most uses. It is like using a canon to kill a fly. It imposes too many limitations (limited API surface), forces you to jump through hoops to achieve some very basic tasks.” While Jason argues in this blog post that you will thank him later for “your lack of #ifdef and spaghetti code.” But Miguel points out that “Jason does not like #if statements on his shared code. But this is not the norm, it is an exception. Not only it is an exception, but careful use of partial classes in C# make this a non issue.”

So I tried to look for examples of what Miguel was talking about, but the only thing I found was this blog post that showed one way to do this. However looking over that blog post, it did not seem any better than a PCL, in other words the way the solution was laid out seemed less than ideal to me. In that example, the author had a common platform agnostic code file that was linked into both an Android and iOS class library project. The platform agnostic linked file contained a partial class with a partial method declaration for the method that will need platform specific code, and the implementation of the platform specific version of the method was in the same class library. In other words, each iOS and Android class library project used the same platform agnostic partial class file, and then the iOS Class Library project had the partial class with the iOS implementation of the partial method defined in the platform agnostic partial class, and the Android Class Library project had the partial class with the Android implementation. Here is what the solution  explorer looks like with this method:

Although this does achieve avoiding the use of #if compiler directives, it adds complexity in that now you have to have two class library projects in addition to the Shared project. Plus with this method, if one were to start a new from template Xamarin.Forms (Shared) solution, they would also have to add Android and iOS Class libraries, and link in just the partial classes that need platform specific implementations to the iOS and Android class libraries, and have the implementation there. This, to me, adds unneeded complexity.

I see two possible solutions, one avoids the #if directives while the other uses #if directives, but in a possibly less obtrusive way.

For the former option, i.e. avoiding #if directives completely, one could just reference the Forms Shared project from the platform projects, and then in the platform projects implement the partial classes and methods needed by the platform agnostic partial classes in the shared code that have the partial method declarations. With this solution, there would be no #ifdef or #if (the former checks if the compiler symbol exists, the latter checks the value) directives and no Class Library projects. However there is a snag in that partial methods must be private and must return void, so you will need to use an instance variable to store any value needed from the platform specific implementation, and make a public method that will call into the platform specific partial method and return the instance variable that was set by the platform specific method. As an example, I will use getting a file path for a filename. On Android you might use code like this:

string libraryPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); ;
_filepath = Path.Combine(libraryPath, filename);

And on iOS:

string documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); // Documents folder
string libraryPath = Path.Combine(documentsPath, "..", "Library"); // Library folder
_filepath = "iOS: " + Path.Combine(libraryPath, filename);

So what I am going to do is create an new Class file called Files in each of the projects, i.e. the Shared project, the Android project, and the iOS Project, but make sure they are all using the same namespace. First the partial class in the Shared project would look like this:

using System;

namespace SharedProjectNoIFDEF
{
	public partial class Files
	{
		public Files()
		{
			
		}

		string _filepath;
		public string GetFilePath(string filename)
		{
			PlatformGetFilePath(filename);
			return _filepath;
		}
		partial void PlatformGetFilePath(string filename);
	}
}

And then in the iOS project:

using System;
using System.IO;

namespace SharedProjectNoIFDEF
{
	public partial class Files
	{
		partial void PlatformGetFilePath(string filename)
		{
			// we need to put in /Library/ on iOS5.1 to meet Apple's iCloud terms
			// (they don't want non-user-generated data in Documents)
			string documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); // Documents folder
			string libraryPath = Path.Combine(documentsPath, "..", "Library"); // Library folder

			_filepath = Path.Combine(libraryPath, filename);
		}
	}
}

And in the Android project:

using System;
using System.IO;

namespace SharedProjectNoIFDEF
{
	public partial class Files
	{
		partial void PlatformGetFilePath(string filename)
		{
			// Just use whatever directory SpecialFolder.Personal returns
			string libraryPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); ;

			_filepath = Path.Combine(libraryPath, filename);
		}
	}
}

Finally to call that method:

var path = new Files().GetFilePath("filename.png");

So that is one way to do it with no #if or #ifdef directives, but is that really any better that using Dependency Injection? You still have your platform specific code in the platform app project, however there is the advantage of the larger .NET API surface in your shared code as noted by Miguel.

For the latter alternative, i.e. keeping all of the code in the Shared project and using #if in a possibly less obtrusive way, you could just have all of the code in the Files class in the Shared project, e.g.:

using System;
using System.IO;

namespace SharedProjectNoIFDEF
{
	public partial class Files
	{
		public Files()
		{

		}

		string _filepath;
		public string GetFilePath(string filename)
		{
			PlatformGetFilePath(filename);
			return _filepath;
		}
		partial void PlatformGetFilePath(string filename);
	}
#if __IOS__
	public partial class Files
	{
		partial void PlatformGetFilePath(string filename)
		{
			// we need to put in /Library/ on iOS5.1 to meet Apple's iCloud terms
			// (they don't want non-user-generated data in Documents)
			string documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); // Documents folder
			string libraryPath = Path.Combine(documentsPath, "..", "Library"); // Library folder

			_filepath = "iOS: " + Path.Combine(libraryPath, filename);
		}
	}
#else
#if __ANDROID__
	public partial class Files
	{
		partial void PlatformGetFilePath(string filename)
		{
			// Just use whatever directory SpecialFolder.Personal returns
			string libraryPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); ;

			_filepath = "Android: " + Path.Combine(libraryPath, filename);
		}
	}
#endif
#endif

}

At least here your code itself is not punctuated with #if or #ifdef directives so this might be slightly more attractive than the alternative, e.g.:

public string GetFilePath(string filename)
{
#if __IOS__
    // we need to put in /Library/ on iOS5.1 to meet Apple's iCloud terms
    // (they don't want non-user-generated data in Documents)
     string documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); // Documents folder
     string libraryPath = Path.Combine(documentsPath, "..", "Library"); // Library folder

#else
#if __ANDROID__
    // Just use whatever directory SpecialFolder.Personal returns
     string libraryPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); ;

#endif
#endif
     return Path.Combine(libraryPath, filename);

}

In the end I really think it is a matter of taste and some code complexity seems to be par for the course in cross platform application development no matter which method you use to share code between app platforms.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.