How to Make Portable Class Libraries Work for You

Portable Class Library is a .NET library that can be used (in binary form, without recompiling) on multiple .NET platforms.  When you create a Portable Class Library in Visual Studio, you can choose which platforms to target.  Portable libraries support targeting the .NET Framework, Silverlight, Windows Phone, Windows Store apps, and XBox 360 XNA games.

Portable Class Library dialog to choose target frameworks 
Portable Class Library creation dialog in Visual Studio 2012


This can be a big improvement over creating a separate project file for each platform you want to target, and keeping them all in sync.  For example, shown below is the project structure for the MVVM Light Toolkit.

MVVM Light Toolkit Project Structure
MVVM Light Toolkit Project Structure


I bet it’s not fun to keep all those projects in sync each time a file needs to be added, removed, or renamed.  There are only three logical libraries here (the core library, extras, and the tests).  Wouldn’t it be great to just have one portable version of each of those libraries instead of seven different ones?

Trouble in Paradise

Unfortunately, it’s not that simple.  The fact that Portable Class Libraries need to run on multiple platforms imposes some constraints on what you can do in them.  Many .NET Framework APIs (such as File I/O or anything to do with the user interface) aren’t available.  Portable libraries can’t reference non-portable libraries.  You can’t use P/Invoke from most portable libraries.  This is all because portable libraries are supposed to actually run on different platforms, but it means it can be more complicated to write them, and can in some cases be very difficult (or not possible) to convert existing .NET libraries to portable libraries.

We’ve seen lots of people excited about portable libraries, but we’ve also seen some people who have run into these limitations questioning whether portable libraries are useful for them.  In this post, I’ll cover how you can use Portable Class Libraries even when you run into these limitations.  First, let’s look at the reasons some APIs aren’t supported, and what functionality actually is supported.

 

Why APIs Aren’t Portable

API Y U NO PORTABLE

Here are some of the reasons APIs aren’t available in portable libraries (taken from David Kean’s MSDN article on Creating a Continuous Client Using Portable Class Libraries):

The API Isn’t Implemented by All Platforms 
Traditional .NET Framework file IOs, such as System.IO.File and System.IO.Directory, fall into this bucket. Silverlight and Windows Phone use the System.IO.IsolatedStorage APIs (though different from the .NET Framework version), whereas Windows Store apps use Windows.Storage.

The API Isn’t Compatible Across All Platforms 
Some APIs look and feel the same, but it’s hard or impossible to write code against them in a portable and consistent way. ThreadStaticAttribute, which enables static fields to have a unique value for each thread, is an example. Though it’s present on both the Windows Phone and Xbox platforms, neither of their runtimes supports it.

The API Is Considered Obsolete or Legacy 
These APIs either contain behavior that’s unlikely to be present on future platforms, or they’ve been replaced by newer technologies. BackgroundWorker is an example of this; it was replaced by Task and the new asynchronous program features in Visual Studio 2012.

We Ran out of Time 
Most APIs weren’t written with portability in mind. We spend a significant amount of time going through each API to make sure it can be programmed against in a portable manner. This might involve tweaking or adding to the API to make it portable. Because of the time and effort involved, in the first version of PCLs we made available on Visual Studio Gallery, we prioritized the high-value, highly used APIs. System.Xml.Linq.dll and System.ComponentModel.DataAnnotations.dll are examples of APIs that weren’t available in that first version but are now available in the Visual Studio 2012 release.

What Is Supported in Portable Libraries?

For a .NET Framework API to be available in a portable library, the API needs to be supported on all the platforms the portable library is targeting.  This means that the platforms you choose to target in your portable library affect the APIs that are available to you.  In general, the more platforms you choose to support, the fewer APIs will be available to you.  For example, the Task type from the TPL is available in .NET 4 and up, Silverlight 5, and Windows Store apps.  So if you target only those platforms you will be able to use the Task type in your portable library.  If you target Silverlight 4, XBox, or Windows Phone 7, you won’t be able to use it.

The following chart gives a summary of what API areas are available in portable libraries and on what platforms.

Portable Class Library Platform and Feature Matrix
Portable Class Library Platform/Feature Support Matrix


The full list of APIs available in portable libraries as of Visual Studio 2012 RTM is available in this spreadsheet: Portable Class Library APIs

Solving Problems With Indirection

Any problem in computer science can be solved with another layer of indirection. – David Wheeler (probably)

What should you do when you’re trying to write a portable library but you need some functionality that isn’t supported?  You can’t call the API directly, and you can’t reference a library that does, because portable libraries can’t reference non-portable libraries.  The solution is to create an abstraction in your portable library that provides the functionality you need, and to implement that abstraction for each platform your portable library targets.  For example, if you need to save and load text files, you might use an interface like this:

public interface IFileStorage
{
     Task SaveFileAsync( string filename, string contents);
     Task<String> LoadFileAsync( string filename);
}

It’s a good idea to include only the functionality you need in the abstraction.  In this example, the interface doesn’t abstract general file system concepts such as streams, folders, or enumerating files.  This makes the abstraction more portable and easier to implement.  The methods return Tasks so that the implementation for Windows Store apps can call the WinRT file IO APIs, which are async.

Creating an abstraction allows portable libraries to call into non-portable code, and this pattern is applicable almost any time you need to access non-portable functionality from a portable library.  Of course, you need some way for the portable code to get a reference to an implementation of the abstraction.  How you do that can depend on whether you are writing a cross platform app or a general purpose reusable library.

Cross Platform Apps

When writing a cross platform app in .NET, you should create the user interface separately for each platform to take advantage of the screen sizes and UI metaphors of each platform.  To avoid duplicating work, you’d like most of the rest of your code to be shared across platforms.  A clean separation between your user interface and the rest of your code will make this easier, and a great pattern for this is the Model-View-View Model (MVVM) pattern.

Diagram of the MVVM Pattern
Diagram of the MVVM Pattern


Using the MVVM pattern, you can share models and view models across platforms using Portable Class Libraries.  When a model or view model needs to access non-portable functionality, you can create an abstraction for that functionality and implement it in platform-specific code.  The app for each platform will contain the views and reference the portable library, as shown in the following diagram.

Cross Platform App Project Structure with Portable Class Libraries and MVVM
Cross Platform App Project Structure with Portable Class Libraries and MVVM


A simple example of a cross platform app that uses portable libraries and MVVM can be found in thisPortableNotepad sample.  In that app, the view model needs access to an instance of IFileStorage, so it takes it as a constructor parameter.  The view’s constructor creates the view model, passing in the platform-specific implementation of IFileStorage, and sets the view’s DataContext to the view model it created.

For more complicated apps, it can get messy passing the implementations for the platform specific functionality down into portable code.  One solution to this is a simple “service locator” in the portable library with static properties corresponding to the portable abstractions, which get set during app startup.  Portable code can then access the non-portable functionality through the service locator class.

public class ServiceLocator
{
     public static IFileStorage FileStorage { get ; set ; }
     public static IPhotoChooser PhotoChooser { get ; set ; }
}

Of course, using static properties like this may raise “singleton pattern” alarm bells, and will make it harder to write unit tests for your code.  So you may want to use an IoC container, as hooking up dependencies while maintaining testability and separation of concerns is what IoC containers are designed to do.  Autofacis one IoC container which has a portable version, and there’s also a portable fork of Ninject.

David Kean’s MSDN article on Creating a Continuous Client Using Portable Class Libraries includes a sample which demonstrates a more complex cross platform app which uses portable libraries and MVVM.  The sample is a shopping list app called OnYourWayHome and has a Windows Phone 7 and a Windows Store app version.  It shows how you can handle navigation between pages, use an IoC container, and use Azure Service bus to sync state between multiple clients.  The version from the MSDN article is based on the VS 2012 Beta and Windows 8 Consumer Preview, so it will need some updates to run on Windows 8 RTM.  An updated version of the app which uses MEF Lightweight composition (instead of Autofac) is included as a sample in the MEF source code.

Another sample app is Contoso Helpdesk, which I’ve been using in presentations about Portable Class Libraries.  This app uses the same architecture (and much of the same code) as the OnYourWayHome sample.  Like OnYourWayHome, it has a Windows Phone 7 and Windows Store app version, but instead of a shopping list, it keeps track of helpdesk tickets.  In Contoso Helpdesk, I’ve factored out the MVVM infrastructure pieces which could be reused in other apps into a library called MvvmPortable.  You can use this library as a starting point to write your own cross platform apps.

Reusable Portable Libraries

 

 

If you are creating a library for others to use (such as an open source library), you probably want to make it as easy as possible to reference and use your library.  That means consumers of the library should be able to add a reference to it as a single unit.  Some libraries won’t need any non-portable functionality, so you can create a single portable DLL.  Autofac and Json.NET are two libraries which have done this.  For libraries that need to be factored into multiple DLLs (for example a portable and a platform specific part), publishing them as NuGet packages allows consumers to reference the different components of the library as one unit.  (NuGet also makes it a lot easier to consume a library that consists of a single assembly, so either way I recommend publishing your libraries on NuGet.)

The code to use your library should also be simple.  That means the project referencing your library shouldn’t have to manage hooking up the platform specific part of your library with the portable part.  One way to do this is to have an initialize method in your platform specific part of your library which passes references to platform specific implementations down to the portable part of your library.  Here’s an example of what this could look like:

public class MyLibraryPhoneUtil
{
     public static void Initialize()
     {
         MyLibraryServiceLocator.FileStorage = new PhoneFileStorage();
         MyLibraryServiceLocator.PhotoChooser = new PhonePhotoChooser();
     }
}

This is simple for you as a library author to write, but it means that consumers of your library need to call the initialize method on startup.

For consumers of your library, it will be easier if the portable part of your library connects to the platform specific part with no action required on their part.  You can do this by having the portable part load the platform-specific part by calling Assembly.Load(), and using reflection to get the functionality it needs.  For an example of how to do this, see  the Portable Class Libraries Contrib project.  The code which handles this functionality is in the Source\Portable.Runtime\Adaptation folder.  The Reactive Extensions (Rx) also use this method to connect their portable System.Reactive.Core assembly with the platform-specific System.Reactive.PlatformServices assembly.  Speaking of Reactive Extensions, the team’s blog posts announcing Rx 2.0 BetaRC, and RTM cover how they converted Rx to support Portable Class Libraries, and are a great case study for how to do so with a large existing library.

Another option is to use reflection to call platform-specific APIs from a portable library directly.  This can be a good choice for a library which can be almost entirely portable, but which has a small, isolated set of platform-specific functionality it needs to access.  This portable fork of MVVM Light uses this method in the ViewModelBase class to determine whether it is running in the designer.

Doing the Impossible

Portable libraries can’t reference non-portable libraries, because anything a portable library references needs to run on the same platforms the portable library supports.  But what if there is a library that is supported on all the platforms a portable library needs, it just has a separate DLL for each platform instead of being implemented as a Portable Class Library?  Wouldn’t it be nice if you could reference that library from a portable library?  With a bit of work, you can.

To use a concrete example, let’s imagine there’s a .NET library for sqlite that you would like to use from a portable library.  The trick is to reference one assembly when compiling, and deploy a different one to be used at runtime.  You can create a portable library with the same API surface and assembly identity as the sqlite library.  This is your “reference assembly.”  This assembly should never be used at runtime, so you don’t actually have to implement anything—just throw NotImplementedExceptions in all methods.  Then a portable library can reference and compile against the reference assembly version of the sqlite library.  Apps that use the portable library can reference the normal implementation of the sqlite library for the platform they target, which will cause that version of the sqlite library to be packaged with the app and used at runtime.

I don’t know of any projects that are currently using this strategy, and I’m not sure if overall it’s likely to be a better solution than creating a portable abstraction like I described previously.  Still, it’s an interesting option.  I’d love to hear how it works for you if you do try using it.  If it turns out to be a useful pattern we can consider providing more tooling to support it.

Conclusion

Using the patterns I’ve described, Portable Class Libraries can be used for a wide variety of projects, both for cross platform apps and general purpose reusable libraries.  Of course, the fact that these patterns are needed in the first place means that using portable libraries can be more complicated than writing code for a single platform.  If you want to target multiple platforms, however, you will have to deal with additional complexity somehow.  You can choose to keep entirely separate code bases; share your code with linked source files, separate projects for each platform, and conditional compilation; or use portable libraries to share code between platforms.  There are advantages and disadvantages to each of these options.  I think in many cases, portable libraries are a great choice—they avoid the project proliferation problem and make it easier to share code in a way that will be maintainable in the long run.

Portable Class Library solution structure sample from MvvmCross
Sample of how Portable Class Libraries can simplify the solution structure for a cross platform app –MvvmCross


The functionality supported in portable libraries will continue to grow over time.  At the beginning of 2011, we released the first version of Portable Class Library support as an add-in for Visual Studio 2010 with support for a fairly limited set of APIs.  With the Visual Studio 2012 release, we now support much more functionality (which is also now available in the add-in for Visual Studio 2010).  We will continue to work on making more code portable across more platforms using portable libraries.  We also hope to see third-party libraries which provide portable abstractions and implementations for you so you have less work to do when writing a cross-platform app (Sqlite and Xamarin.Mobile would be great candidates for this).

We’d also love to hear your feedback.  We hope you will try using Portable Class Libraries and let us know what problems you run into and what seems to be more difficult than it should be.  This will help us know what to focus on for future improvements.  You can leave a comment on this blog post or contact David Kean and me on Twitter.

For more information on Portable Class Libraries, see this Overview of Portable Class Libraries on the .NET Framework Blog, and the links from this Channel 9 Visual Studio Toolbox show on Portable Class Libraries.

 

from: http://blogs.msdn.com/b/dsplaisted/archive/2012/08/27/how-to-make-portable-class-libraries-work-for-you.aspx

你可能感兴趣的:(Class)