Home About Eric Topics SourceGear

2014-10-06 10:00:00

Fiddling with ReactiveUI and Xamarin.Forms

I'm no expert at ReactiveUI. I'm just fiddling around in the back row of a session here at Xamarin Evolve. :-)

The goal

I want an instance of my DataGrid control that is bound to a collection of objects. And I want the display to automatically update when I change a property on one of those objects.

For the sake of this quickie demo, I'm gonna add a tap handler for a cell that simply appends an asterisk to the text of that cell. That should end up causing a display update.

Something like this code snippet:


Main.SingleTap += (object sender, CellCoords e) => {
    T r = Rows [e.Row];
    ColumnInfo ci = Columns[e.Column];
    var typ = typeof(T);
    var ti = typ.GetTypeInfo();
    var p = ti.GetDeclaredProperty(ci.PropertyName);
    if (p != null)
    {
        var val = p.GetValue(r);
        p.SetValue(r, val.ToString() + "*");
    }
};

Actually, that looks complicated, because I've got some reflection code that figures out which property on my object corresponds to which column. Ignore the snippet above and think of it like this (for a tap in column 0):

Main.SingleTap += (object sender, CellCoords e) => {
    WordPair r = Rows [e.Row];
    r.en += "*";
};

Which will make more sense if you can see the class I'm using to represent a row:

public class WordPair
{
    public string en { get; set; }
    public string sp { get; set; }
}

Or rather, that's what it looked like before I started adapting it for ReactiveUI. Now it needs to notify somebody when its properties change, so it looks more like this:

public class WordPair : ReactiveObject
{
    private string _en;
    private string _sp;

    public string en {
        get { return _en; }
        set { this.RaiseAndSetIfChanged (ref _en, value); }
    }
    public string sp {
        get { return _sp; }
        set { this.RaiseAndSetIfChanged (ref _sp, value); }
    }
}

So, basically I want a DataGrid which is bound to a ReactiveList. But actually, I want it to be more generic than that. I want WordPair to be a type parameter.

So my DataGrid subclass of Xamarin.Forms.View has a type parameter for the type of the row:

public class ColumnishGrid<T> : Xamarin.Forms.View where T : class

And the ReactiveList<T> is stored in a property of that View:

public static readonly BindableProperty RowsProperty = 
    BindableProperty.Create<ColumnishGrid<T>,ReactiveList<T>>(
        p => p.Rows, null);

public ReactiveList<T> Rows {
    get { return (ReactiveList<T>)GetValue(RowsProperty); }
    set { SetValue(RowsProperty, value); } // TODO disallow invalid values
}

And the relevant portions of the code to build a Xamarin.Forms content page look like this:

var mainPage = new ContentPage {
    Content = new ColumnishGrid<WordPair> {

        ...

        Rows = new ReactiveList<WordPair> {
            new WordPair { en = "drive", sp = "conducir" },
            new WordPair { en = "speak", sp = "hablar" },
            new WordPair { en = "give", sp = "dar" },
            new WordPair { en = "be", sp = "ser" },
            new WordPair { en = "go", sp = "ir" },
            new WordPair { en = "wait", sp = "esperar" },
            new WordPair { en = "live", sp = "vivir" },
            new WordPair { en = "walk", sp = "andar" },
            new WordPair { en = "run", sp = "correr" },
            new WordPair { en = "sleep", sp = "dormir" },
            new WordPair { en = "want", sp = "querer" },
        }
    }
};

The implementation of ColumnishGrid contains the following snippet, which will be followed by further explanation:

IRowList<T> rowlist = new RowList_Bindable_ReactiveList<T>(this, RowsProperty);

IValuePerCell<string> vals = new ValuePerCell_RowList_Properties<string,T>(rowlist, propnames);

IDrawCell<IGraphics> dec = new DrawCell_Text (vals, fmt);

In DataGrid, a RowList is an interface used for binding some data type that represents a whole row.

public interface IRowList<T> : IPerCell
{
    bool get_value(int r, out T val);
}

A RowList It could be an array of something (like strings), using the column number as an index. But in this case, I am using a class, with each property mapped to a column.

A ValuePerCell object is used anytime DataGrid needs, er, a value per cell (like the text to be displayed):

public interface IValuePerCell<T> : IPerCell
{
    bool get_value(int col, int row, out T val);
}

And ValuePerCell_RowList_Properties is an object which does the mapping from a column number (like 0) to a property name (like WordPair.en).

Then the ValuePerCell object gets handed off to DrawCell_Text, which is what actually draws the cell text on the screen.

I skipped one important thing in the snippet above, and that's RowList_Bindable_ReactiveList. Since I'm storing my ReactiveList in a property on my View, there are two separate things to listen on. First, I obviously want to listen for changes to the ReactiveList and update the display appropriately. But I also need to listen for the case where somebody replaces the entire list.

RowList_Bindable_ReactiveList handles the latter, so it has code that looks like this:

obj.PropertyChanged += (object sender, System.ComponentModel.PropertyChangedEventArgs e) => {
    if (e.PropertyName == prop.PropertyName)
    {
        ReactiveList<T> lst = (ReactiveList<T>)obj.GetValue(prop);
        _next = new RowList_ReactiveList<T>(lst);
        if (changed != null) {
            changed(this, null);
        }
        _next.changed += (object s2, CellCoords e2) => {;
            if (changed != null) {
                changed(this, e2);
            }
        };
    }
};

And finally, the code which listens to the ReactiveList itself:

public RowList_ReactiveList(ReactiveList<T> rx)
{
    _rx = rx;

    _rx.ChangeTrackingEnabled = true;
    _rx.ItemChanged.Subscribe (x => {
        if (changed != null) {
            int pos = _rx.IndexOf(x.Sender);
            changed(this, new CellCoords(0, pos));
        }
    });
}

DataGrid uses a chain of drawing objects which pass notifications up the chain with a C# event. In the end, the DataGrid core panel will hear about the change, and it will trigger the renderer, which will cause a redraw.

And that's how the word "give" ended up with an asterisk in the screen shot at the top of this blog entry.