Search code examples
xamarin.androidxamarinmvvmcrossmvxbind

mvxlistview click on item


I'm currently creating an app for Android using MvvmCross. Part of that app requires a MvxListView, where each item has 2 TextViews. One of these TextViews are hidden by default. I'm looking to implement an accordion like functionality, where clicking on the first TextView will show/hide the other TextView.

I've currently gotten most of this to work with the MvvmCross Visibility Plugin, but the click event is bound to the MvxListView instead of the TextView inside it. What I've currently gotten to work looks like this:

FirstViewModel:
public class FirstViewModel
    : MvxViewModel
{
    public FirstViewModel(IListService listService)
    {
        Interests = new ObservableCollection<Interest>();
        List<Interest> tempInterests = listService.GetInterestFeeds("");
        foreach (var interest in tempInterests)
        {
            interest._parent = this;
            Interests.Add(interest);
        }
        var pluginLoader = new PluginLoader();
        pluginLoader.EnsureLoaded();
    }
    private ObservableCollection<Interest> _interests;
    public ObservableCollection<Interest> Interests
    {
        get { return _interests; }
        set { _interests = value; RaisePropertyChanged(() => Interests); }
    }

    public ICommand ItemVisibleCommand
    {
        get
        {
            return new MvxCommand<Interest>(item => item.IsVisible = !item.IsVisible);
        }
    }
}

FirstView:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:local="http://schemas.android.com/apk/res-auto"
    ...>
    <Mvx.MvxListView
        ...
        local:MvxBind="ItemsSource Interests"
        local:MvxItemTemplate="@layout/item_interests" />
</LinearLayout>

item_interests:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:local="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textSize="30dp"
        local:MvxBind="Text InterestName" />
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textSize="20dp"
        local:MvxBind="Text InterestDescription; Visibility IsVisible, Converter=Visibility" />
</LinearLayout>

In order to bind it to the TextView inside the MvxListView, I've been trying to modify my code to something similar to How to bind ItemClick in MvxListView in MvxListView as per the anwser by Stuart, resulting in the following code:

item_interest:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:local="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textSize="30dp"
        local:MvxBind="Text InterestName; Click ItemVisibleCommand" />
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textSize="20dp"
        local:MvxBind="Text InterestDescription; Visibility IsVisible, Converter=Visibility" />
</LinearLayout>

FirstViewModel:

public class FirstViewModel
    : MvxViewModel
{
    public FirstViewModel(IListService listService)
    {
        Interests = new ObservableCollection<Interest>();
        List<Interest> tempInterests = listService.GetInterestFeeds("");
        foreach (var interest in tempInterests)
        {
            interest._parent = this;
            Interests.Add(interest);
        }
        var pluginLoader = new PluginLoader();
        pluginLoader.EnsureLoaded();
    }
    private ObservableCollection<Interest> _interests;
    public ObservableCollection<Interest> Interests
    {
        get { return _interests; }
        set { _interests = value; RaisePropertyChanged(() => Interests); }
    }

    public void MakeItemVisible(bool isVisible)
    {
        isVisible = !isVisible;
    }

Interest:

public class Interest : INotifyPropertyChanged
{
    public string InterestId { get; set; }
    public string InterestName { get; set; }
    public string InterestDescription { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;

    public FirstViewModel _parent { get; set; }

    private bool _isVisible;
    public bool IsVisible
    {
        get { return _isVisible; }
        set
        {
            _isVisible = value;
            onPropertyChanged(this, "IsVisible");
        }
    }

    private void onPropertyChanged(object sender, string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            PropertyChanged(sender, new PropertyChangedEventArgs(propertyName));
        }
    }


    public Interest(string id, string name, string description)
    {
        //Initialisers
    }



    private MvxCommand<bool> _itemVisible;
    public ICommand ItemVisibleCommand
    {
        get
        {
            _itemVisible = _itemVisible ?? new MvxCommand<bool>(IsVisible => _parent.MakeItemVisible(IsVisible));
            return _itemVisible;
        }
    }
}

resulting in the following exception:

04-04 15:05:40.575 I/MonoDroid(18011): UNHANDLED EXCEPTION: System.NullReferenceException: Object reference not set to an instance of an object
04-04 15:05:40.575 I/MonoDroid(18011): at Cirrious.MvvmCross.ViewModels.MvxCommand`1<bool>.Execute (object) <IL 0x00010, 0x00088>
04-04 15:05:40.575 I/MonoDroid(18011): at Cirrious.MvvmCross.Binding.Droid.Target.MvxViewClickBinding.ViewOnClick (object,System.EventArgs) <IL 0x0001f, 0x000fb>
04-04 15:05:40.575 I/MonoDroid(18011): at Android.Views.View/IOnClickListenerImplementor.OnClick (Android.Views.View) [0x0000d] in /Users/builder/data/lanes/monodroid-mlion-monodroid-4.12-series/a1e3982a/source/monodroid/src/Mono.Android/platforms/android-15/src/generated/Android.Views.View.cs:1615
04-04 15:05:40.575 I/MonoDroid(18011): at Android.Views.View/IOnClickListenerInvoker.n_OnClick_Landroid_view_View_ (intptr,intptr,intptr) [0x00011] in /Users/builder/data/lanes/monodroid-mlion-monodroid-4.12-series/a1e3982a/source/monodroid/src/Mono.Android/platforms/android-15/src/generated/Android.Views.View.cs:1582
04-04 15:05:40.575 I/MonoDroid(18011): at (wrapper dynamic-method) object.a963c1ac-b573-4022-b41d-f0f002438c84 (intptr,intptr,intptr) <IL 0x00017, 0x00043>
Unhandled Exception:
System.NullReferenceException: Object reference not set to an instance of an object

Thanks in advance to anyone who's taken the time to read all that :)


UPDATE - I tried to do as Stuart suggested, and got the following solution: First off, to preserve the original Interest entity, it got wrapped in an InterestWrapper.

public class InterestWrapper : INotifyPropertyChanged
{
    private Interest _interest;
    private InterestAndroidViewModel _parent;  //TO-DO
    public Interest Item { get { return _interest; } }

    public event PropertyChangedEventHandler PropertyChanged;

    private bool _isVisible;
    public bool IsVisible
    {
        get { return _isVisible; }
        set
        {
            _isVisible = value;
            onPropertyChanged(this, "IsVisible");
        }
    }

    private void onPropertyChanged(object sender, string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            PropertyChanged(sender, new PropertyChangedEventArgs(propertyName));
        }
    }
    public InterestWrapper(Interest interest, InterestAndroidViewModel parent)
    {
        IsVisible = false;
        _interest = interest;
        _parent = parent;
    }

    public IMvxCommand ItemVisibleCommand
    {
        get
        {
            return new MvxCommand(() => _parent.MakeItemVisible(_interest));
        }
    }
}

FirstViewModel

public class FirstViewModel
    : MvxViewModel
{
    public FirstViewModel(IListService listService)
    {
        Interests = new ObservableCollection<InterestWrapper>();
        List<Interest> tempInterests = listService.GetInterestFeeds("");
        foreach (var interest in tempInterests)
        {
            InterestWrapper wrapper = new InterestWrapper(interest, this);
            Interests.Add(wrapper);
        }
    }
    private ObservableCollection<InterestWrapper> _interests;
    public ObservableCollection<InterestWrapper> Interests
    {
        get { return _interests; }
        set { _interests = value; RaisePropertyChanged(() => Interests); }
    }

    public void MakeItemVisible(Interest interest)
    {
        if (interest.IsVisible)
        {
            interest.IsVisible = !interest.IsVisible;
        }
        else
        {
            foreach (var _interest in _interests)
            {
                _interest.Item.IsVisible = false;
            }
            interest.IsVisible = !interest.IsVisible;
        }
    }
}

item_interest:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:local="http://schemas.android.com/apk/res-auto"
    ...
    <RelativeLayout
        ...
        <TextView
            ...
            local:MvxBind="Text Item.InterestName; Click ItemVisibleCommand" />
        <Mvx.MvxImageView
            ...
            local:MvxBind="Visibility Item.IsVisible, Converter=InvertedVisibility; Click ShowEducationsCommand" />
    </RelativeLayout>
</RelativeLayout>

Solution

  • The local:MvxBind="Text InterestName; Click ItemVisibleCommand" can only really call a non-parameterized MvxCommand - it can't call MvxCommand<bool> as it doesn't know what the bool value is.

    If you wanted to, you could use the CommandParameter converter to pass in the value - e.g. local:MvxBind="Text InterestName; Click CommandParameter(ItemVisibleCommand, IsVisible)"

    But overall, in this case I'd probably recommend rewriting ItemVisibleCommand as just a "toggle visible" command instead