Creating Mobile Apps with Xamarin.Forms: Infrastructure
- 10/1/2014
Version 3. Going async
The data isn’t persisted for Windows Phone because the file I/O code is not correct. A Xamarin.Forms application targets Windows Phone 8, which implements a subset of the same WinRT file I/O available to Windows 8 applications, largely found in the new Windows.Storage and Windows.Storage.-Streams namespaces.
Windows Phone 8 continues to support some older file I/O functions that Windows Phone 7 inherited from Silverlight, but these are not recommended for new Windows Phone 8 applications. Windows Phone 8 applications should instead use the WinRT file I/O API, and the programs in this book follow that recommendation.
Part of the impetus behind this new array of file I/O classes in Windows 8 and Windows Phone 8 is a recognition of a transition away from the relatively unconstrained file access of desktop applications towards a more sandboxed environment. To store data that is private to an application, a Windows Phone program first gets a special StorageFolder object:
StorageFolder
localFolder =ApplicationData
.Current.LocalFolder;
ApplicationData has a static property named Current that returns the ApplicationData object for the application. LocalFolder is an instance property of ApplicationData.
StorageFolder defines methods named CreateFileAsync to create a new file and GetFileAsync to open an existing file. These two methods return objects of type StorageFile. At this point, a program can open the file for writing or reading with OpenAsync or OpenReadAsync. These methods return an IRandomAccessStream object. From this, DataWriter or DataReader objects are created to perform write or read operations.
This sounds a bit lengthy, and it is. A rather simpler approach for text files involves the static methods FileIO.ReadTextAsync and FileIO.WriteTextAsync. The first argument to these methods is a StorageFile object, and the methods incorporate all the operations to open the file, write to it or read from it, and close the file. Although these methods are available in Windows Phone 8.1, they are not in Windows Phone 8, the version of Windows Phone supported by Xamarin.Forms.
At any rate, by this time you’ve undoubtedly noticed the frequent Async suffix on these method names. These are asynchronous methods. Internally, these methods spin off secondary threads of execution for doing the actual work and return quickly to the caller. The work takes place in the background, and when that work is finished—when the file has been created or opened, or written to or read from—the caller is notified through a call-back function and provided with the function’s result.
Which might raise the question: Why are these methods asynchronous? Why are they more complex than the old .NET file I/O functions?
Graphical user interfaces have an intrinsic problem. Although an application can consist of multiple threads of execution, access to the user interface must usually be restricted to a single thread. The problem is not so much the graphical output, but the user input, which in various environments might include the keyboard, mouse, pen, or touch. In general, it can’t be known where a user input event should be routed until all previous user input events have been processed. This means that user input events must be processed sequentially in a single thread of execution.
The impact of this restriction is profound: In the general case, all the application’s user interface processing must be handled in a single thread, often called the UI thread. Even the seemingly innocent act of using a secondary thread of execution to set a property of a user-interface object such as Entry or Editor is forbidden.
At the same time, programmers are cautioned against doing any lengthy processing in this UI thread. If the UI thread is carrying out some lengthy processing, it can’t respond to user input events and the entire user interface can seem to freeze up.
As we users have become more accustomed to graphical user interfaces over the decades, we’ve become increasingly intolerant of even the slightest lapse in responsiveness. Consequently, as application programmers, we are increasingly encouraged to avoid lengthy processing in the UI thread and to keep the application as responsive as possible.
This implies that lengthy processing jobs should be relegated to secondary threads of execution. These threads run “in the background,” asynchronously with the UI thread.
The future of computing will undoubtedly involve a lot more asynchronous computing and parallel processing, particularly with the increasing use of multicore processor chips. Developers will need good language tools to work with asynchronous operations, and fortunately C# has been in the forefront in this regard.
When the WinRT APIs used for Windows 8 Store apps were being developed, the Microsoft developers took a good hard look at timing and decided that any function call that could require more than 50 milliseconds to execute should be made asynchronous so that it would not interfere with the responsiveness of the user interface.
APIs that require more than 50 milliseconds obviously include file I/O functions, which often need to access potentially slow pieces of hardware, like disk drives or a network. Any WinRT file I/O function that could possibly hit a physical storage device was made asynchronous and given an Async method suffix.
The CreateFileAsync method defined by the StorageFolder class does not directly return a StorageFile object. Instead, it returns an IAsyncOperation<StorageFile> object:
IAsyncOperation
<StorageFile
> createOp = storageFolder.CreateFileAsync("filename"
);
The IAsyncOperation interface, its base interface IAsyncInfo, and related interfaces such as IAsyncAction, are all defined in the Windows.Foundation namespace, indicating how fundamental they are to the entire operating system. A return value such as IAsyncOperation is sometimes referred to as a “promise.” The StorageFile object is not available just yet, but it will be in the future if nothing goes awry.
To begin the actual asynchronous operation, you must assign a handler to the Completed property of the IAsyncOperation object:
createOp.Completed = OnCreateFileCompleted;
Completed is a property rather than an event but it functions much like an event. The big difference is that Completed can’t have multiple handlers. Assigning a callback method (named OnCreateFile-Completed in this example) actually initiates the background process.
The code that sets the Completed property to a handler executes very quickly, and the program can then continue normally. Simultaneously, the file is being created in a secondary thread. When the file is created, that background code calls the callback method assigned to the Completed handler in your code. That callback method might look like this:
void
OnCreateFileCompleted(IAsyncOperation
<StorageFile
> createOp,AsyncStatus
asyncStatus) {if
(asyncStatus ==AsyncStatus
.Completed) {StorageFile
storageFile = createOp.GetResults();// continue with next step ...
}else
{// deal with cancellation or error
} }
The second argument indicates the status, and at this point it’s either Completed, Canceled, or Error. Members of the first argument can provide more detail about any error that might have occurred. If all is well, calling GetResults on the first argument obtains the StorageFile object.
At this point, the next step would be to open that file for writing. A call to OpenAsync returns an object of type IAsyncOperation<IRandomAccessStream>, and that involves another callback method:
void
OnCreateFileCompleted(IAsyncOperation
<StorageFile
> createOp,AsyncStatus
asyncStatus) {if
(asyncStatus ==AsyncStatus
.Completed) {StorageFile
storageFile = createOp.GetResults();IAsyncOperation
<IRandomAccessStream
> openOp = storageFile.OpenAsync(FileAccessMode
.ReadWrite); openOp.Completed = OnOpenFileCompleted; }else
{// deal with cancellation or error
} }void
OnOpenFileCompleted(IAsyncOperation
<IRandomAccessStream
> openOp,AsyncStatus
asyncStatus) {// ...
}
One way to simplify this code is to use anonymous lambda functions for the callbacks. This avoids a proliferation of individual methods and allows more free-form access to local variables. But for a sequence of asynchronous method calls, it tends to produce a nested structure of asynchronous callbacks, as you’ll see.
Asynchronous lambdas in the SAP
In the NoteTaker3Sap project, the file I/O code has been moved to the Note class and performed separately for Windows Phone using lambda functions for callbacks. To keep the code simple (at least comparatively so), there is no error handling:
using
System;#if
!WINDOWS_PHONEusing
System.IO;#else
using
Windows.Foundation;using
Windows.Storage;using
Windows.Storage.Streams;#endif
namespace
NoteTaker3Sap {class
Note
{public
string
Title {set
;get
; }public
string
Text {set
;get
; }public
void
Save(string
filename) {string
text =this
.Title +"\n"
+this
.Text;#if
!WINDOWS_PHONE// iOS and Android
string
docsPath =Environment
.GetFolderPath(Environment
.SpecialFolder
.Personal);string
filepath = Path.Combine(docsPath, filename); File.WriteAllText(filepath, text);#else
// Windows Phone
StorageFolder
localFolder =ApplicationData
.Current.LocalFolder;IAsyncOperation
<StorageFile
> createOp = localFolder.CreateFileAsync(filename,CreationCollisionOption
.ReplaceExisting); createOp.Completed = (asyncInfo1, asyncStatus1) => {IStorageFile
storageFile = asyncInfo1.GetResults();IAsyncOperation
<IRandomAccessStream
> openOp = storageFile.OpenAsync(FileAccessMode
.ReadWrite); openOp.Completed = (asyncInfo2, asyncStatus2) => {IRandomAccessStream
stream = asyncInfo2.GetResults();DataWriter
dataWriter =new
DataWriter
(stream); dataWriter.WriteString(text);DataWriterStoreOperation
storeOp = dataWriter.StoreAsync(); storeOp.Completed = (asyncInfo3, asyncStatus3) => { dataWriter.Dispose(); }; }; };#endif
}public
void
Load(string
filename) {#if
!WINDOWS_PHONE// iOS and Android
string
docsPath =Environment
.GetFolderPath(Environment
.SpecialFolder
.Personal);string
filepath = Path.Combine(docsPath, filename);string
text = File.ReadAllText(filepath);// Break string into Title and Text.
int
index = text.IndexOf('\n'
);this
.Title = text.Substring(0, index);this
.Text = text.Substring(index + 1);#else
// Windows Phone
StorageFolder
localFolder =ApplicationData
.Current.LocalFolder;IAsyncOperation
<StorageFile
> createOp = localFolder.GetFileAsync(filename); createOp.Completed = (asyncInfo1, asyncStatus1) => {IStorageFile
storageFile = asyncInfo1.GetResults();IAsyncOperation
<IRandomAccessStreamWithContentType
> openOp = storageFile.OpenReadAsync(); openOp.Completed = (asyncInfo2, asyncStatus2) => {IRandomAccessStream
stream = asyncInfo2.GetResults();DataReader
dataReader =new
DataReader
(stream);uint
length = (uint
)stream.Size;DataReaderLoadOperation
loadOp = dataReader.LoadAsync(length); loadOp.Completed = (asyncInfo3, asyncStatus3) => {string
text = dataReader.ReadString(length); dataReader.Dispose();// Break string into Title and Text.
int
index = text.IndexOf('\n'
);this
.Title = text.Substring(0, index);this
.Text = text.Substring(index + 1); }; }; };#endif
} } }
Notice the Dispose calls on the DataWriter and DataReader methods. It might be tempting to remove these calls under the assumption that the objects are disposed automatically when the objects go out of scope, but this is not the case. If Dispose is not called, the files remain open,
But the big question is: Why has all this code been moved to the Note class? Why isn’t it isolated in a separate FileHelper class as in the previous version of the program?
The problem is that a method that requires asynchronous callbacks to obtain an object can’t directly return that object to the caller. Look at the Load method here. When that Load method is called in a Windows Phone program, the localFolder variable is set, and the createOp object is set, but as soon as the Completed property is set to the asynchronous callback method, the Load method returns to the caller. But the method doesn’t yet have anything to return! The GetFileAsync operation is proceeding in the background in a secondary thread. Only later does the Completed callback method execute for the next step of the job. When the contents of the file are finally read within these nested callbacks, the contents must be stored somewhere. Fortunately, the code is in the Note class, so the results can be stored in the Title and Text properties.
In the previous version of this program, the Exists method returned a Boolean to indicate the existence of a file. That code needs to be moved to the NoteTaker3SapPage class where it has access to the Button whose IsEnabled property must be set:
using
System;#if
!WINDOWS_PHONEusing
System.IO;#else
using
Windows.Foundation;using
Windows.Storage;using
Windows.Storage.Streams;#endif
using
Xamarin.Forms;namespace
NoteTaker3Sap {class
NoteTaker3SapPage
:ContentPage
{static
readonly
string
FILENAME ="test.note"
;Note
note =new
Note
();Entry
entry;Editor
editor;Button
loadButton;public
NoteTaker3SapPage() {// 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 =false
, HorizontalOptions =LayoutOptions
.CenterAndExpand }; loadButton.Clicked += OnLoadButtonClicked;// Check if the file is available.
#if
!WINDOWS_PHONE// iOS and Android
string
docsPath =Environment
.GetFolderPath(Environment
.SpecialFolder
.Personal);string
filepath = Path.Combine(docsPath, FILENAME); loadButton.IsEnabled = File.Exists(filepath);#else
// Windows Phone
StorageFolder
localFolder =ApplicationData
.Current.LocalFolder;IAsyncOperation
<StorageFile
> createOp = localFolder.GetFileAsync(FILENAME); createOp.Completed = (asyncInfo, asyncStatus) => { loadButton.IsEnabled = asyncStatus !=AsyncStatus
.Error; };#endif
// 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.Title = entry.Text; note.Text = editor.Text; note.Save(FILENAME);this
.loadButton.IsEnabled =true
; }void
OnLoadButtonClicked(object
sender,EventArgs
args) { note.Load(FILENAME); entry.Text = note.Title; editor.Text = note.Text; } } }
To set that IsEnabled property of the Load button in the Windows Phone version, the strategy is to attempt to call GetFileAsync. If that call reports an error in the asynchronous callback, the file does not exist. (The StorageFile class defines an IsAvailable property, but it isn’t supported on Windows Phone.)
Notice that this version of the page class contains a single Note object instantiated as a field and accessed by both Button event handlers. This makes more sense than creating a new Note object in each call to the Clicked handlers. In the final version of the program, when a page like this is used for creating a new note or editing an existing note, the page and the Note object will exist as a tightly-linked pair—one class for the user interface, and another class for the underlying data exposed by the user interface.
If you try out this program on Windows Phone, you’ll still discover a problem:
Type some text in the Entry and Editor. Press the Save button. Now erase that text or change it. Press the Load button. The saved text returns. Great!
Now end the program and start it up again. The Entry and Editor fields are blank but the Load button is enabled. Excellent! The file still exists. Press the Load button. Nothing. That’s odd: If the Button is enabled, the file should exist, so where is it? Now press the Load button a second time. Ahh, there it is!
Can you figure out what’s happening?
When you run the program and press the Load button to load a previously created file, the Load method in Note is called. But that method returns after calling GetFileAsync. The file hasn’t been read yet, the Title and Text properties of Note haven’t yet been set, but the OnLoadButton-Clicked method blithely sets the contents of those Title and Text properties to the Entry and Editor. The callbacks in the Load method in Note continue to execute until the file is read and the Title and Text properties are eventually set, so pressing the Load button a second retrieves them.
This problem shows up only when the program starts up, because thereafter the values in the Note object are always the last values saved to the file.
Method callbacks in the PCL
Can these asynchronous method calls be incorporated in a PCL project that uses the Dependency-Service to access platform-specific versions of the file I/O logic? Certainly not in the same form as in the SAP version. The Exists and ReadAllText methods must return values—a bool and a string, respectively—and we’ve already seen that the asynchronous function calls in a method can’t return values.
But these methods can return values if they return those values in their own callback functions!
Here’s the new IFileHelper interface in the NoteTaker3Pcl project:
using
System;namespace
NoteTaker3Pcl {public
interface
IFileHelper
{void
Exists(string
filename,Action
<bool
> completed);void
WriteAllText(string
filename,string
text,Action
completed);void
ReadAllText(string
filename,Action
<string
> completed); } }
All three methods now have a return type of void, but they all have a last argument that is a delegate for a method with (respectively) one Boolean argument, no arguments, and one string argument.
The iOS implementation of this interface is very similar to the previous version except that the completed method is called to indicate completion and to return any value:
using
System;using
System.IO;using
Xamarin.Forms; [assembly
:Dependency
(typeof
(NoteTaker3Pcl.iOS.FileHelper
))]namespace
NoteTaker3Pcl.iOS {class
FileHelper
:IFileHelper
{public
void
Exists(string
filename,Action
<bool
> completed) {bool
exists =File
.Exists(GetFilePath(filename)); completed(exists); }public
void
WriteAllText(string
filename,string
text,Action
completed) {File
.WriteAllText(GetFilePath(filename), text); completed(); }public
void
ReadAllText(string
filename,Action
<string
> completed) {string
text =File
.ReadAllText(GetFilePath(filename)); completed(text); }string
GetFilePath(string
filename) {string
docsPath =Environment
.GetFolderPath(Environment
.SpecialFolder
.MyDocuments);return
Path
.Combine(docsPath, filename); } } }
The Android version is very similar. The Windows Phone version has methods that call the completed function in the innermost nested asynchronous callback:
using
System;using
Windows.Foundation;using
Windows.Storage;using
Windows.Storage.Streams;using
Xamarin.Forms; [assembly
:Dependency
(typeof
(NoteTaker3Pcl.WinPhone.FileHelper
))]namespace
NoteTaker3Pcl.WinPhone {class
FileHelper
:IFileHelper
{public
void
Exists(string
filename,Action
<bool
> completed) {StorageFolder
localFolder =ApplicationData
.Current.LocalFolder;IAsyncOperation
<StorageFile
> createOp = localFolder.GetFileAsync(filename); createOp.Completed = (asyncInfo, asyncStatus) => { completed(asyncStatus !=AsyncStatus
.Error); }; }public
void
WriteAllText(string
filename,string
text,Action
completed) {StorageFolder
localFolder =ApplicationData
.Current.LocalFolder;IAsyncOperation
<StorageFile
> createOp = localFolder.CreateFileAsync(filename,CreationCollisionOption
.ReplaceExisting); createOp.Completed = (asyncInfo1, asyncStatus1) => {IStorageFile
storageFile = asyncInfo1.GetResults();IAsyncOperation
<IRandomAccessStream
> openOp = storageFile.OpenAsync(FileAccessMode
.ReadWrite); openOp.Completed = (asyncInfo2, asyncStatus2) => {IRandomAccessStream
stream = asyncInfo2.GetResults();DataWriter
dataWriter =new
DataWriter
(stream); dataWriter.WriteString(text);DataWriterStoreOperation
storeOp = dataWriter.StoreAsync(); storeOp.Completed = (asyncInfo3, asyncStatus3) => { dataWriter.Dispose(); completed(); }; }; }; }public
void
ReadAllText(string
filename,Action
<string
> completed) {StorageFolder
localFolder =ApplicationData
.Current.LocalFolder;IAsyncOperation
<StorageFile
> createOp = localFolder.GetFileAsync(filename); createOp.Completed = (asyncInfo1, asyncStatus1) => {IStorageFile
storageFile = asyncInfo1.GetResults();IAsyncOperation
<IRandomAccessStreamWithContentType
> openOp = storageFile.OpenReadAsync(); openOp.Completed = (asyncInfo2, asyncStatus2) => {IRandomAccessStream
stream = asyncInfo2.GetResults();DataReader
dataReader =new
DataReader
(stream);uint
length = (uint
)stream.Size;DataReaderLoadOperation
loadOp = dataReader.LoadAsync(length); loadOp.Completed = (asyncInfo3, asyncStatus3) => {string
text = dataReader.ReadString(length); dataReader.Dispose(); completed(text); }; }; }; } } }
To hide away the calls to the DependencyService.Get method, another file has been added to the NoteTaker3Pcl project. This class lets other code in the program use normal-looking static FileHelper methods:
namespace
NoteTaker3Pcl {static
class
FileHelper
{static
IFileHelper
fileHelper =DependencyService
.Get<IFileHelper
>();public
static
void
Exists(string
filename,Action
<bool
> completed) { fileHelper.Exists(filename, completed); }public
static
void
WriteAllText(string
filename,string
text,Action
completed) { fileHelper.WriteAllText(filename, text, completed); }public
static
void
ReadAllText(string
filename,Action
<string
> completed) { fileHelper.ReadAllText(filename, completed); } } }
Notice that the class also saves the DependencyService object associated with the IFile-Helper interface in a static field to make the actual calls more efficient. Although this class seems to implement the IFileHelper interface, it actually does not implement that interface because the class and methods are all static.
The Note class is now almost as simple as the original version. The only real difference is a Load method that sets the Title and Text fields in a lambda function passed to the FileHelper.Read-AllText method:
namespace
NoteTaker3Pcl {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) {FileHelper
.ReadAllText(filename, (string
text) => {// Break string into Title and Text.
int
index = text.IndexOf('\n'
);this
.Title = text.Substring(0, index);this
.Text = text.Substring(index + 1); }); } } }
The only difference in the page file is the code to determine if the Load button should be disabled. The IsEnabled setting occurs in a lambda function passed to the FileHelper.Exists method:
loadButton =new
Button
{ Text ="Load"
, IsEnabled =false
, HorizontalOptions =LayoutOptions
.CenterAndExpand }; loadButton.Clicked += OnLoadButtonClicked;// Check if the file is available.
FileHelper
.Exists(FILENAME, (exists) => { loadButton.IsEnabled = exists; });
Does this fix the problem with the first press of the Load button on the Windows Phone? No, it does not. The OnLoadButtonClicked method is still setting the Entry and Editor to text from Note class properties before that text has been loaded from the file. To work properly, that code would need to know when the Note properties were set before transferring them to the Editor and Entry. Or the page class would need to know when the Title and Text properties of the Note object changed values.
The basic problem involves the existence of properties that change value without notifying anybody of the change. Can we do something about this?