Creating Mobile Apps with Xamarin.Forms: Infrastructure
- 10/1/2014
Version 2. File input/output
Traditionally, file input/output is one of the most basic programming tasks, but file I/O on mobile devices is a little different than on the desktop. On the desktop, users and applications generally have a whole disk available organized in a directory structure. On mobile devices, several standard folders exist—for pictures or music, for example—but application-specific data is generally restricted to private storage areas.
Programmers familiar with .NET know that the System.IO namespace contains the bulk of standard file I/O support. Perhaps the most important class in this namespace is the static File class, which not only provides a bunch of methods to create new files and open existing files but also includes several methods capable of performing an entire file read or write operation in a single method call.
For example, the File.WriteAllText method has two arguments of type string—a filename and the file contents. The method creates the file (replacing an existing file with the same name if necessary), writes the contents to the file, and closes it. The File.ReadAllText method is similar but returns the contents of the file in one big string object. These methods seem ideal for the job of saving and retrieving notes.
The Xamarin.iOS and Xamarin.Android libraries include a version of .NET that has been expressly tailored by Xamarin for these two mobile platforms. The methods in the File class in the System.IO namespace map to appropriate file I/O functions in the iOS and Android platforms. This means that you can use methods in the File class—including File.WriteAllText and File.ReadAll-Text—in your iPhone and Android applications.
Let’s experiment a bit:
Go into Visual Studio or Xamarin Studio, and load any Xamarin.Forms solution created so far, such as NoteTaker1. Bring up one of the code files in the iOS or Android project. In a constructor or method, type the System.IO namespace name and then a period. You’ll get a list of all the available types in the namespace. If you then type File and a period, you’ll get all the static methods in the File class, including WriteAllText and ReadAllText.
In a Windows Phone project, however, you’re working with a version of .NET created by Microsoft and stripped down somewhat for the Windows Phone platform. If you type System.IO.File and a period, you’ll see a rather diminished File class that does not include WriteAllText and ReadAll-Text, although it does include methods to create and open text files.
Now go into any code file in a Xamarin.Forms Portable Class Library project, and type System.IO and a period. Now you won’t even see the File class! It does not exist in the PCL. Why is that? PCLs are configured to support multiple target platforms. The APIs implemented within the PCL are necessarily an intersection of the APIs in these target platforms.
A PCL appropriate for Xamarin.Forms includes the following platforms:
- .NET Framework 4.5
- Windows 8
- Windows Phone Silverlight 8
- Xamarin.Android
- Xamarin.iOS
Notice the inclusion of Windows 8, which incorporates an API called the Windows Runtime (or WinRT). Microsoft completely revamped file I/O for WinRT and created a whole new file I/O API. The System.IO.File class does not exist in the PCL because it is not part of WinRT.
Although the File class does not exist in a Portable Class Library project, you might wonder what kind of File class you can use in a Shared Asset Project. Well, it varies by what platform is being compiled. You can use File.WriteAllText and File.ReadAllText in your iOS and Android projects but not in your Windows Phone projects. Your Windows Phone projects need something else.
Skip past the scary stuff?
Already you might suspect that this subject of file I/O is going to get hairy, and you are correct. But consider what we’re trying to do here: We’re trying to target three different mobile platforms with a common code base. That’s not easy, and we’re bound to encounter some rough terrain along the way.
So the question has to be: Metaphorically speaking, do you enjoy hiking through treacherous terrain to climb to the top of a mountain and get a gorgeous view? Or would you prefer that somebody else takes a photo from the top of the mountain and sends it to you in an email?
If you’re in the latter category, you might prefer to skip over much of this chapter and jump straight to the comparatively simple and elegant solution to the problem of Xamarin.Forms file I/O presented at the chapter’s end and continuing into the next chapter. Until that point, some of the transitional code you’ll encounter will be both scary and ugly.
If you choose to take this long path up the mountain, you’ll understand why the trip is necessary, and you’ll understand the rationale behind the platform differences—even in seemingly routine jobs like file I/O.
In the previous chapter you saw how the Xamarin.Forms Device class can be a valuable tool for dealing with platform differences. But the code that’s referenced by the Device class must be compilable in all three platforms. This is not the case for file I/O because the different platforms have access to different APIs. This means that the platform differences can’t be managed using the Device class and must be handled in other ways. Moreover, the solutions are different for Shared Asset Projects and Portable Class Libraries projects.
For this reason, for the next two versions of NoteTaker, there will be separate solutions for SAP and PCL. The two different solutions for version 2 are named NoteTaker2Sap and NoteTaker2Pcl.
Preprocessing in the SAP
Dealing with platform differences is a Shared Asset Project is a little more straightforward than a PCL and involves more traditional programming tools, so let’s begin with that.
In code files in a Shared Asset Project, you can use the C# preprocessor directives #if, #elif, #else, and #endif with conditional compilation symbols defined for the three platforms. These symbols are __IOS__ for iOS and WINDOWS_PHONE for Windows Phone; there are no conditional compilation symbols for Android, but Android can be identified as not being iOS or Windows Phone.
The NoteTaker2Sap project includes a class named FileHelper in a file named FileHelper.cs. You can add such a file to the project the same way you add a new file for the class that derives from ContentPage.
The FileHelper.cs file uses C# preprocessor directives to divide the code into two sections. The first section is for iOS and Android and is compiled if the WINDOWS_PHONE identifier is not defined. The second section is for Windows Phone.
Both sections contain three public static methods named Exists, WriteAllText, and ReadAllText. In the first section, the iOS and Android versions of these functions use standard static File methods but with a folder name obtained from the Environment.GetFolderPath method with an argument of Environment.SpecialFolder.MyDocuments:
namespace
NoteTaker2Sap {static
class
FileHelper
{#if
!WINDOWS_PHONE// iOS and Android
public
static
bool
Exists(string
filename) {string
filepath = GetFilePath(filename);return
File
.Exists(filepath); }public
static
void
WriteAllText(string
filename,string
text) {string
filepath = GetFilePath(filename);File
.WriteAllText(filepath, text); }public
static
string
ReadAllText(string
filename) {string
filepath = GetFilePath(filename);return
File
.ReadAllText(filepath); }static
string
GetFilePath(string
filename) {string
docsPath =Environment
.GetFolderPath(Environment
.SpecialFolder
.MyDocuments);return
Path
.Combine(docsPath, filename); }#else
// Windows Phone
public
static
bool
Exists(string
filename) {return
File
.Exists(filename); }public
static
void
WriteAllText(string
filename,string
text) {StreamWriter
writer =File
.CreateText(filename); writer.Write(text); writer.Close(); }public
static
string
ReadAllText (string
filename) {StreamReader
reader =File
.OpenText(filename);string
text = reader.ReadToEnd(); reader.Close();return
text; }#endif
} }
When the Shared Asset Project is compiled for Windows Phone, the File.WriteAllText and File.ReadAllText methods do not exist so those can’t appear in the Windows Phone section. However, static CreateText and OpenText methods are available, and these are used to obtain StreamWriter and StreamReader objects. This Windows Phone code works in the sense that it doesn’t raise an exception, but you’ll see shortly that it really doesn’t do what you want. Something else is required.
Besides the FileHelper class to handle low-level file I/O, the NoteTaker2Sap project includes another new class named Note. This class encapsulates the two string objects associated with a note in simple read/write properties named Title and Text. This class also includes methods named Save and Load that call the appropriate methods in FileHelper:
namespace
NoteTaker2Sap {class
Note
{public
string
Title {set
;get
; }public
string
Text {set
;get
; }public
void
Save(string
filename) {string
text =this
.Title +"\n"
+this
.Text; FileHelper.WriteAllText(filename, text); }public
void
Load(string
filename) {string
text = FileHelper.ReadAllText(filename);// Break string into Title and Text.
int
index = text.IndexOf('\n'
);this
.Title = text.Substring(0, index);this
.Text = text.Substring(index + 1); } } }
Notice that the Save method simply joins the two string objects into one with a line feed character, and the Load method takes them apart.
Finally, the NoteTaker2SapPage class has a very similar page layout as the first program but contains two buttons labeled Save and Load that use the Note class for these operations. A filename of “test.note” is used throughout:
class
NoteTaker2SapPage
:ContentPage
{static
readonly
string
FILENAME ="test.note"
;Entry
entry;Editor
editor;Button
loadButton;public
NoteTaker2SapPage() {// Create Entry and Editor views.
entry =new
Entry
{ Placeholder ="Title (optional)"
}; editor =new
Editor
{ Keyboard =Keyboard
.Create(KeyboardFlags
.All), BackgroundColor =Device
.OnPlatform(Color
.Default,Color
.Default,Color
.White), VerticalOptions =LayoutOptions
.FillAndExpand };// Create Save and Load buttons.
Button
saveButton =new
Button
{ Text ="Save"
, HorizontalOptions =LayoutOptions
.CenterAndExpand }; saveButton.Clicked += OnSaveButtonClicked; loadButton =new
Button
{ Text ="Load"
, IsEnabled =FileHelper
.Exists(FILENAME), HorizontalOptions =LayoutOptions
.CenterAndExpand }; loadButton.Clicked += OnLoadButtonClicked;// Assemble page.
this
.Padding =new
Thickness
(10,Device
.OnPlatform(20, 0, 0), 10, 0);this
.Content =new
StackLayout
{ Children = {new
Label
{ Text ="Title:"
}, entry,new
Label
{ Text ="Note:"
}, editor,new
StackLayout
{ Orientation =StackOrientation
.Horizontal, Children = { saveButton, loadButton } } } }; }void
OnSaveButtonClicked(object
sender,EventArgs
args) {Note
note =new
Note
{ Title = entry.Text, Text = editor.Text }; note.Save(FILENAME); loadButton.IsEnabled =true
; }void
OnLoadButtonClicked(object
sender,EventArgs
args) {Note
note =new
Note
(); note.Load(FILENAME); entry.Text = note.Title; editor.Text = note.Text; } }
Notice that the Load button initialization calls FileHelper.Exists to determine if the file exists and disables the button if it does not. The button is then enabled the first time the file is saved.
You’ll want to convince yourself that this works—that the information is saved to the device (or phone simulator). Type something into the Entry and Editor, and then press Save to save that information. Clear out the Entry and Editor (or type in new text), and press Load to restore the information that was saved.
Here’s what’s really important: If you terminate the program or shut down the phone and then rerun the program, the saved file still exists.
Well, in two out of three cases, the saved file still exists. It works with iOS and Android, but not Windows Phone. Although the Windows Phone Save and Load buttons seem to work while the program is running, the file is not persisted when the application is exited. Getting Windows Phone to work right will require a different set of file I/O classes.
Meanwhile, let’s try to get this simple (two-thirds functional) version to work with a Portable Class Library solution.
Dependency service in the PCL
As you’ve seen, the System.IO.File class does not exist in the version of .NET available to a Xamarin.Forms Portable Class Library. This means that if you’ve created a PCL-based Xamarin.Forms solution, the file I/O code cannot be in the PCL. The file I/O code must be implemented in the individual platform projects where it can use the version of .NET specifically for that platform.
Yet, the PCL must somehow make calls to these file I/O functions. Normally that wouldn’t work: Application projects make calls to libraries all the time, but libraries generally can’t make calls to applications except with events or callback functions.
It is the main purpose of the Xamarin.Forms DependencyService class to get around this restriction. Although this class is implemented in the Xamarin.Forms.Core library assembly and used in a PCL, it uses .NET reflection to search through all the other assemblies available in the application, including the particular platform-specific application assembly itself. (It is also possible for the platform projects to use dependency injection techniques to configure the PCL to make calls into the platform projects.)
To use DependencyService, the first requirement is that the PCL must contain an interface definition that includes the names and signatures of the platform-specific methods you need. Here is that file in the NoteTaker2Pcl project:
namespace
NoteTaker2Pcl
{public
interface
IFileHelper
{bool
Exists(string
filename);void
WriteAllText(string
filename,string
text);string
ReadAllText(string
filename); } }
This interface must be public to the PCL because it must be visible to the individual platform projects.
Next, in all three application projects, you create code files with classes that implement this interface. Here’s the one in the iOS project. (You can tell that this file is in the iOS project by the namespace):
using
System;using
System.IO;using
Xamarin.Forms; [assembly
:Dependency
(typeof
(NoteTaker2Pcl.iOS.FileHelper
))]namespace
NoteTaker2Pcl.iOS {class
FileHelper
:IFileHelper
{public
bool
Exists(string
filename) {string
filepath = GetFilePath(filename);return
File
.Exists(filepath); }public
void
WriteAllText(string
filename,string
text) {string
filepath = GetFilePath(filename);File
.WriteAllText(filepath, text); }public
string
ReadAllText(string
filename) {string
filepath = GetFilePath(filename);return
File
.ReadAllText(filepath); }string
GetFilePath(string
filename) {string
docsPath =Environment
.GetFolderPath(Environment
.SpecialFolder
.MyDocuments);return
Path
.Combine(docsPath, filename); } } }
The actual implementation of these methods involves the same code that you’ve already seen except with instance methods rather than static methods. But take note of two necessary characteristics of this file:
- The class implements the IFileHelper interface defined in the PCL. Because it implements that interface, the three methods defined in the interface must be defined as public in this class.
- A special assembly-level attribute named Dependency is defined prior to the namespace definition.
Dependency is a special Xamarin.Forms attribute defined by the DependencyAttribute class specifically for use with the DependencyService class. The Dependency attribute simply specifies the type of the class but it assists the DependencyService class in locating the implementation of the interface in the application projects.
A similar file is in the Android project:
using
System;using
System.IO;using
Xamarin.Forms; [assembly
:Dependency
(typeof
(NoteTaker2Pcl.Droid.FileHelper
))]namespace
NoteTaker2Pcl.Droid {class
FileHelper
:IFileHelper
{public
bool
Exists(string
filename) {string
filepath = GetFilePath(filename);return
File
.Exists(filepath); }public
void
WriteAllText(string
filename,string
text) {string
filepath = GetFilePath(filename);File
.WriteAllText(filepath, text); }public
string
ReadAllText(string
filename) {string
filepath = GetFilePath(filename);return
File
.ReadAllText(filepath); }string
GetFilePath(string
filename) {string
docsPath =Environment
.GetFolderPath(Environment
.SpecialFolder
.MyDocuments);return
Path
.Combine(docsPath, filename); } } }
And in the Windows Phone project:
using
System;using
System.IO;using
Xamarin.Forms; [assembly
:Dependency
(typeof
(NoteTaker2Pcl.WinPhone.FileHelper
))]namespace
NoteTaker2Pcl.WinPhone {class
FileHelper
:IFileHelper
{public
bool
Exists(string
filename) {return
File
.Exists(filename); }public
void
WriteAllText(string
filename,string
text) {StreamWriter
writer =File
.CreateText(filename); writer.Write(text); writer.Close(); }public
string
ReadAllText(string
filename) {StreamReader
reader =File
.OpenText(filename);string
text = reader.ReadToEnd(); reader.Close();return
text; } } }
Now the hard work is done. You’ll recall that the Note class in NoteTaker2Sap made calls to FileHelper.WriteAllText and FileHelper.ReadAllText. The Note class in NoteTaker2Pcl is very similar but instead references the two methods through the static DependencyService.Get method. This is a generic method that requires the interface as a generic argument but then is capable of calling any method in that interface:
namespace
NoteTaker2Pcl {class
Note
{public
string
Title {set
;get
; }public
string
Text {set
;get
; }public
void
Save(string
filename) {string
text =this
.Title +"\n"
+this
.Text;DependencyService
.Get<IFileHelper
>().WriteAllText(filename, text); }public
void
Load(string
filename) {string
text =DependencyService
.Get<IFileHelper
>().ReadAllText(filename);// Break string into Title and Text.
int
index = text.IndexOf('\n'
);this
.Title = text.Substring(0, index);this
.Text = text.Substring(index + 1); } } }
Internally, the DependencyService class searches for the interface implementation in the particular platform project and makes a call to the specified method.
The NoteTaker2PclPage class is nearly the same as NoteTaker2SapPage except it also uses DependencyService.Get to call the Exists method during the initialization of the Load button.
loadButton =new
Button
{ Text ="Load"
, IsEnabled =DependencyService
.Get<IFileHelper
>().Exists(FILENAME), HorizontalOptions =LayoutOptions
.CenterAndExpand };
Of course, the NoteTaker2Pcl version has the same deficiency as NoteTaker2Sap in that it doesn’t persist the data for Windows Phone.