Creating Mobile Apps with Xamarin.Forms: Infrastructure
- 10/1/2014
- Version 1. The Entry and Editor views
- Version 2. File input/output
- Version 3. Going async
- Version 4. I will notify you when the property changes
- Version 5. Data binding
- Version 6. Awaiting results
- What's next?
Version 4. I will notify you when the property changes
Let’s step back a moment.
So far, all the versions of the program have contained a class deriving from ContentPage that displays a user interface allowing a user to enter and edit two pieces of text. These two pieces of text are also stored in a class named Note. This Note class stores data that underlies the user interface of the page class.
These two classes are really two sides of the same data—one class presents the data for editing by the user, and the other class handles the more low-level chores, including loading and saving the data in the file system.
Optimally, at any time, both classes should be dealing with the same data. But this is not the case. The page class doesn’t know when the data in the Note class has changed, and the Note class doesn’t know when the text in the Entry and Editor views has changed.
Keeping user interfaces in synchronization with underlying data is a common problem, and standard solutions are available to fix that problem. One of the most important is an interface named INotifyPropertyChanged. It’s defined in the .NET System.ComponentModel namespace like so:
interface
INotifyPropertyChanged
{event
PropertyChangedEventHandler
PropertyChanged; }
The entire interface consists of just one event named PropertyChanged, but this event provides a simple universal way for a class to notify any other class that might be interested when one of its properties has changed values.
The PropertyChangedEventHandler delegate associated with the PropertyChanged event incorporates an event argument of PropertyChangedEventArgs. This class defines a public property of type string named PropertyName that identifies the property being changed.
What’s that? A property named PropertyName that identifies the property being changed? Yes, it sounds a little confusing, but in practice it’s quite simple.
The following NoteTaker4 program was created with the PCL template, but a Shared Asset Project could implement these changes as well.
A class such as Note can implement the INotifyPropertyChanged interface by simply indicating that the class derives from that interface and including a public event of the correct type and name:
class
Note
:INotifyPropertyChanged
{public
event
PropertyChangedEventHandler
PropertyChanged; ... }
In theory, that’s all that’s required. However, a class that implements this interface should also actually fire the event whenever one of its public properties changes value. The PropertyChangedEventArgs object accompanying the event identifies the actual property that’s changed value. The property should have been assigned its new value by the time it fires the event.
In the previous versions of the Note class, the properties were defined with implicit backing fields:
public
string
Title {set
;get
; }public
string
Text {set
;get
; }
Now they’re going to need explicit private backing fields:
string
title, text;
Here’s the new definition of the Title property. The Text property is similar:
public
string
Title {set
{if
(title !=value
) { title =value
;if
(PropertyChanged !=null
) { PropertyChanged(this
,new
PropertyChangedEventArgs
("Title"
)); } } }get
{return
title; } }
This is very standard INotifyPropertyChanged code. The set accessor begins by checking if the private field is the same as the incoming string, and only continues if it’s not. Some programmers new to INotifyPropertyChanged want to skip this check, but it’s important. The interface is called INotifyPropertyChanged and not INotifyMaybePropertyChangedMaybeNot. In some cases, failing to check if the property is actually changing can cause infinite recursion.
The set accessor continues by saving the new value in the backing field and only then firing the event.
Here’s the complete Note class:
using
System.ComponentModel;namespace
NoteTaker4 {class
Note
:INotifyPropertyChanged
{string
title, text;public
event
PropertyChangedEventHandler
PropertyChanged;public
string
Title {set
{if
(title !=value
) { title =value
;if
(PropertyChanged !=null
) { PropertyChanged(this
,new
PropertyChangedEventArgs
("Title"
)); } } }get
{return
title; } }public
string
Text {set
{if
(text !=value
) { text =value
;if
(PropertyChanged !=null
) { PropertyChanged(this
,new
PropertyChangedEventArgs
("Text"
)); } } }get
{return
text; } }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 various FileHelper classes are the same as those in NoteTaker3Pcl.
The NoteTaker4Page class defines an instance of Note as a field (as in the previous version of the program), but now the constructor also attaches a handler for the PropertyChanged event now defined by Note:
note.PropertyChanged += (sender, args) => {switch
(args.PropertyName) {case
"Title"
: entry.Text = note.Title;break
;case
"Text"
: entry.Text = note.Text;break
; } };
This could be a named event handler of course, and it could use an if and else rather that a switch and case to identify the property being changed. It then sets the new value of the property to the Text property of either the Entry or Editor.
It looks fine, but it still won’t work on the Windows Phone. When you tap the Load button you’ll get an Unauthorized Access exception. Now what’s wrong?
Here’s the problem: In the general case, callbacks from asynchronous methods do not execute in the same thread as the code that the initiated the operation. Instead, the callback executes in the background thread that carried out the asynchronous operation.
Let’s follow it through: When you press the Load button, the Windows Phone ReadAllText method executes. When the text is obtained, it calls the completed method but in a secondary thread of execution. In the Load method in Note, that completed method sets the Title and Text properties. The new Title property causes a PropertyChanged event to fire, and in that handler the new Title property is set to the Text property of the Entry view.
Therefore, the Entry view is being accessed from a thread other than the user-interface thread, and that’s not allowed. That’s why the exception is raised.
Fortunately, the fix for this problem is fairly easy. The Device class has a BeginInvokeOnMainThread method with an argument of type Action. Simply enclose the code you want to execute in the UI thread in the body of that Action argument. It’s easiest to wrap the entire switch and case in that callback:
note.PropertyChanged += (sender, args) => {Device
.BeginInvokeOnMainThread(() => {switch
(args.PropertyName) {case
"Title"
: entry.Text = note.Title;break
;case
"Text"
: editor.Text = note.Text;break
; } }); };
The Device.BeginInvokeOnMainThread effectively waits until the UI thread gets a time slice from the operating system’s thread scheduler, and then it runs the specified code.
With this change, you’ll find that when you rerun the Windows Phone app, you can press Load just once and the Entry and Editor will be set with the saved values. They’re being set not in the handler for the Load button but in the PropertyChanged handler when the properties are actually updated with the values loaded from the file.
You can also go the other way and keep the Note class updated with the current values of the Entry and Editor views. Simply install TextChanged handlers:
entry.TextChanged += (sender, args) => { note.Title = args.NewTextValue; }; editor.TextChanged += (sender, args) => { note.Text = args.NewTextValue; };
Wait a minute. Have we messed this up? The PropertyChanged handler is setting the Entry and Editor text from the Note properties, and now these two TextChanged handlers are setting the Note properties from the Entry and Editor text. Isn’t that an infinite loop?
No, because Entry, Editor, and Note fire Changed events only when the property is actually changing. The potentially infinite loop is truncated when the corresponding properties are the same.
Now that the Entry and Editor views are kept consistent with the Note class, it’s not necessary to set the Note object from the Entry and Editor in the Save handler. Nor do we need to set the Entry and Editor from the Note object in the Load handler. Here’s the complete NoteTaker4-Page class. Notice that the Entry and Editor instances no longer need to be saved as fields because they’re no longer referenced in the Clicked handlers:
class
NoteTaker4Page
:ContentPage
{static
readonly
string
FILENAME ="test.note"
;Note
note =new
Note
();Button
loadButton;public
NoteTaker4Page() {// Create Entry and Editor views.
Entry
entry =new
Entry
{ Placeholder ="Title (optional)"
};Editor
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.
FileHelper
.Exists(FILENAME, (exists) => { loadButton.IsEnabled = exists; });// Handle the Note's PropertyChanged event.
note.PropertyChanged += (sender, args) => {Device
.BeginInvokeOnMainThread(() => {switch
(args.PropertyName) {case
"Title"
: entry.Text = note.Title;break
;case
"Text"
: editor.Text = note.Text;break
; } }); };// Handle the Entry and Editor TextChanged events.
entry.TextChanged += (sender, args) => { note.Title = args.NewTextValue; }; editor.TextChanged += (sender, args) => { note.Text = args.NewTextValue; };// 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.Save(FILENAME); loadButton.IsEnabled =true
; }void
OnLoadButtonClicked(object
sender,EventArgs
args) { note.Load(FILENAME); } }
As you test this new version, you might want to restore the phone or simulator to a state where no file has yet been saved. You can do that simply by uninstalling the application from the phone or simulator. That uninstall removes all the data stored along with the application as well.