Code review: Concise views with static-type bindings

So I wrote this, and I'm pretty happy with it, and I thought I'd share. I hate magic strings, and I'm a big fan of static typing, and I usually build my interfaces in code rather than XAML. But at the same time, I don't like specifying types where they can be inferred. So here are a couple helper classes I made to help out:

public struct ViewModelBinder<TViewModel> {
    readonly BindableObject target;

    public ViewModelBinder(BindableObject target)
    {
        this.target = target;
    }

    public ViewModelBinder<TViewModel> Bind(Expression<Func<TViewModel, object>> sourceProperty,
        BindingMode mode = 0, IValueConverter converter = null, string stringFormat = null)
    {
        // Add more default target properties here as needed
        var targetProperty = target is Entry ? Entry.TextProperty
            : target is DatePicker ? DatePicker.DateProperty
            : target is Image ? Image.SourceProperty
            : target is TextCell ? TextCell.TextProperty : null;
        return Bind(targetProperty, sourceProperty, mode, converter, stringFormat);
    }

    public ViewModelBinder<TViewModel> Bind(BindableProperty targetProperty, Expression<Func<TViewModel, object>> sourceProperty,
        BindingMode mode = 0, IValueConverter converter = null, string stringFormat = null)
    {
        target.SetBinding<TViewModel>(targetProperty, sourceProperty, mode, converter, stringFormat);
        return this;
    }

    public BindableObject Target { get { return target; } }
}

public abstract class BoundContentPage<TViewModel> : ContentPage where TViewModel : class {
    protected BoundContentPage(TViewModel viewModel)
    {
        BindingContext = viewModel;
    }

    protected void Bind(BindableProperty targetProperty, Expression<Func<TViewModel, object>> sourceProperty,
        BindingMode mode = 0, IValueConverter converter = null, string stringFormat = null)
    {
        this.SetBinding<TViewModel>(targetProperty, sourceProperty, mode, converter, stringFormat);
    }

    protected void Bind(BindableObject target, BindableProperty targetProperty, Expression<Func<TViewModel, object>> sourceProperty,
        BindingMode mode = 0, IValueConverter converter = null, string stringFormat = null)
    {
        target.SetBinding<TViewModel>(targetProperty, sourceProperty, mode, converter, stringFormat);
    }

    public T Control<T>(T obj, Action<ViewModelBinder<TViewModel>> init) where T : BindableObject
    {
        init(new ViewModelBinder<TViewModel>(obj));
        return obj;
    }

    public TViewModel ViewModel {
        get { return BindingContext as TViewModel; }
        set { BindingContext = value; }
    }
}

Using BoundContentPage instead of ContentPage, along with Control(), allows me to create things concisely:

        Content = new TableView {
            Intent = TableIntent.Form,
            Root = new TableRoot {
                new TableSection("Section") {
                    Control(new TextCell(), b => b.Bind(vm => vm.SomeTextProperty)),
                },
                new TableSection("NextSection") {
                    Control(new TextCell(), b => b.Bind(vm => vm.AnotherTextProperty)),
                },
            },
        };

Note that I use struct for my ViewModelBinder helper so it gets allocated on the stack and has no GC implications. In cases like DataTemplate I just instantiate the VMB directly since it's hard to avoid typing the name of the type of the underlying object.

            ItemTemplate = new DataTemplate(() => {
                return new ViewModelBinder<StaffListPageItem>(new ImageCell())
                    .Bind(vm => vm.FullName)
                    .Bind(ImageCell.ImageSourceProperty, vm => vm.ImageUri)
                    .Target;
            }),

Thoughts?

Sign In or Register to comment.