Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use the MvvmCross fluent API to bind a RecyclerView item's TextView to a property of its ViewModel on Android?

I am using MvvmCross in my Xamarin Android project. I have an MvxActivity with an MvxRecyclerView, that I have assigned an item template in its layout file.

<MvxRecyclerView
    android:id="@+id/my_recycler_view"
    local:MvxItemTemplate="@layout/item_recycler_view" />

The ViewModel is quite simple, it consists just of one property that holds the data to display in the RecyclerView:

public class MainViewModel : MvxViewModel
{
    private IEnumerable<ViewModelItem> _viewModelItems;
    public IEnumerable<ViewModelItem> ViewModelItems
    {
        get { return _viewModelItems; }
        set { SetProperty(ref _viewModelItems, value); }
    }    
}

Generally I like to use the MvvmCross fluent API as much as possible because of the implicit refactoring support. So in my activity, I am binding a property of the MvxRecyclerView like this:

var recyclerView = View.FindViewById<MvxRecyclerView>(Resource.Id.my_recycler_view);
var set = this.CreateBindingSet<MainView, MainViewModel>();
set.Bind(recyclerView)
    .For(v => v.ItemsSource)
    .To(vm => vm.ViewModelItems);
set.Apply();

So far so good. Now, the layout file for the item template basically just contains a TextView:

<LinearLayout>
    <TextView
        android:id="@+id/innerText" />
</LinearLayout>

And my ViewModelItem class looks like this:

public class ViewModelItem
{
    public string Title { get; set; }
}

My question now is, how and where do I bind the TextView.Text property to the ViewModelItem.Title property using the fluent API?

I know it is quite easy to do without the fluent API by supplying an MvxBind attribute in the item template layout file, but I would really prefer a fluent API solution.

like image 360
lauxjpn Avatar asked Oct 10 '16 15:10

lauxjpn


2 Answers

Inherit from MvxRecyclerAdapter and create a custom Adapter for your RecyclerView. Override OnCreateViewHolder and return a custom ViewHolder.

public class MyAdapter : MvxRecyclerAdapter
{
    public MyAdapter(IMvxAndroidBindingContext bindingContext)
        : base(bindingContext)
    {
    }

    public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
    {
        var itemBindingContext = new MvxAndroidBindingContext(parent.Context, this.BindingContext.LayoutInflaterHolder);
        var view = this.InflateViewForHolder(parent, viewType, itemBindingContext);

        return new MyViewHolder(view, itemBindingContext);
    }
}

Within this ViewHolder you can use the Fluent API for binding.

public class MyViewHolder : MvxRecyclerViewHolder
{
    private readonly TextView textView;

    public MyViewHolder(View itemView, IMvxAndroidBindingContext context)
        : base(itemView, context)
    {
        this.textView = itemView.FindViewById<TextView>(Android.Resource.Id.Text1);

        this.DelayBind(() =>
        {
            var set = this.CreateBindingSet<MyViewHolder, ViewModelItem>();
            set.Bind(this.textView).To(x => x.Title);
            set.Apply();
        });
    }
}

In your Activity create the Adapter and add it to your RecyclerView:

var adapter = new MyAdapter((IMvxAndroidBindingContext)this.BindingContext);
recyclerView.Adapter = adapter;

and bind your Items to the ItemsSource of your Adapter:

set.Bind(this.adapter).For(x => x.ItemsSource).To(x => x.ViewModelItems);
like image 84
Ken Kosmowski Avatar answered Nov 19 '22 04:11

Ken Kosmowski


Based on Ken's answer, I created a couple of support classes and extensions to generalize binding of items and pushed them together with a usage sample to github:

https://github.com/lauxjpn/MvxItemBinder

It allows you to write item bindings like the following:

var recyclerView = FindViewById<MvxRecyclerView>(Resource.Id.RecyclerView);

var set = this.CreateBindingSet<MainActivity, MainViewModel>();
set.Bind(recyclerView)
    .For(v => v.ItemsSource)
    .To(vm => vm.Items);
set.Apply();

recyclerView.BindItems<ItemViewModel>(this, (itemView, itemSet) =>
    itemSet.Bind(itemView.FindViewById<TextView>(Resource.Id.item_template))
        .For(v => v.Text)
        .To(vm => vm.Title)
);

Or even shorter:

var recyclerView = FindViewById<MvxRecyclerView>(Resource.Id.RecyclerView);

var set = this.CreateBindingSet<MainActivity, MainViewModel>();
set.Bind(recyclerView.BindItems<ItemViewModel>(this, (itemView, itemSet) =>
        itemSet.Bind(itemView.FindViewById<TextView>(Resource.Id.item_template))
            .For(v => v.Text)
            .To(vm => vm.Title)))
    .For(v => v.ItemsSource)
    .To(vm => vm.Items);
set.Apply();
like image 28
lauxjpn Avatar answered Nov 19 '22 05:11

lauxjpn