Creating Mobile Apps with Xamarin.Forms: Infrastructure

  • 10/1/2014

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.