How to wire up MvxRecyclerView Item's actions to ViewModel - xamarin

I have a MvxRecyclerView which has the following axml file:
<?xml version="1.0" encoding="utf-8"?>
<MvvmCross.Droid.Support.V7.RecyclerView.MvxRecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:MvxItemTemplate="#layout/item_detail"
app:MvxBind="ItemsSource Items" />
Correspodning ViewModel is defined like this:
public class ItemsViewModel : MvxViewModel
{
private ObservableCollection<Models.Item> _items;
public ObservableCollection<Models.Item> Items
{
get { return _items; }
set
{
_items = value;
RaisePropertyChanged(() => Items);
}
}
public MvxCommand CommandToBeInvokedFromItem
{
get
{
return new MvxCommand(async () =>
{
await ...;
});
}
}
...
}
My item_detail axml is defined like this:
<?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:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24dp"
local:MvxBind="Text Name" />
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/ic_delete_forever_black_24dp"
local:MvxBind="Click CommandToBeInvokedFromItem"/>
</LinearLayout>
And Model.Item is defined like this:
public class Item
{
public string Name { get; set; }
}
First TextView binds to Item's name property which works great. But I want the ImageButton to bind to a Command on the ViewModel to which MvxRecylerView is bound rather than to a property of the Item. Item is just a Model and not a ViewModel. How do I accomplish that?

If you need the command to be called on the click of an item in the MvxRecycler (i.e. the entire cell), the binding is relatively simple. Just change the value of MvxBind on the MvxRecyclerView from ItemsSource Items to ItemsSource Items; ItemClick CommandToBeInvokedFromItem. CommandToBeInvokedFromItem would then need to be modified to accept Item as a type parameter, which would look like this:
public MvxCommand<Models.Item> CommandToBeInvokedFromItem
{
get
{
return new MvxCommand<Models.Item>(async () =>
{
await ...;
});
}
}
If the command needs to be raised specifically by clicking on the ImageButton, then the easiest method would be to move CommandToBeInvokedFromItem to Item, and to have Item inherit MvxViewModel, or at least implement INotifyPropertyChanged.

Hand the command over to the Item when you create it in yourItemsViewModel.
public class Item
{
public string Name { get; set; }
public MvxCommand CommandToBeInvokedFromItem {get;}
public Item(MvxCommand clickCommand)
{
CommandToBeInvokedFromItem = clickCommand;
}
}

Related

MVVMCross Data Binding not working

I have custom object in my ViewModel.
public MyObject PropertyName
{
get { return _propertyName; }
set
{
_propertyName = value;
RaisePropertyChanged(() => PropertyName);
}
}
I want to bind only one of its member to textview in my droid view. Found this link:
Bind data of custom object to TextView in MvvmCross
And I am doing exactly like this. local:MvxBind = "Text Myobject.ItsMember"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:text="LabelCaption" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:textColor="#android:color/white"
local:MvxBind="Text Myobject.ItsMember" />
</RelativeLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
Checked the casing etc. but binding isn't working and I cannot see the string value appearing on the screen while I can print it in the debug window.
Any clues where I may be going wrong?
I cannot reproduce your issue on my end.
I've tried with:
public class MyViewModel : MvxViewModel
{
private MyModel _model; // this is a class member variable or class member field
public MyModel Model // this is a class member property
{
get => _model;
set => SetProperty(ref _model, value);
}
public MyViewModel()
{
Model = new MyModel { Name = "Herp" };
}
}
public class MyModel : MvxNotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
}
Then given that MyViewModel is DataContext I bind using:
local:MvxBind="Text Model.Name"
Works just fine.
You can't bind to a type. You need to bind to an instance of the type. In order for the binding to work the way you have it set in the AXML you need something like
public class MyType
{
private string _ItsMember;
public string ItsMember
{
get
{
return _ItsMember;
}
set
{
_ItsMember = value;
RaisePropertyChanged (()=>ItsMember);
}
}
}
private MyType _MyObject;
public MyType MyObject
{
get
{
return _MyObject;
}
set
{
_MyObject = value;
RaisePropertyChanged(()=>MyObject);
}
}
This arrangement will allow MyObject.ItsMember to resolve to a string property value. If your property is not a string value, you will have to implement a converter which is a whole 'nother subject.

MvvmCross MvxListView ItemClick Enabled Based on property

So I have a MvxListView
<Mvx.MvxListView
android:id="#+id/receptionsListView"
android:divider="#drawable/divider"
android:scrollbars="vertical"
android:choiceMode="singleChoice"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="left|start"
local:MvxItemTemplate="#layout/item_supplier"
local:MvxBind="ItemsSource ReceptionSuppliersList; ItemClick SelectReceptionCommand;" />
I want to enabled/disabled some Items based on a value from model in the list. Something like
local:MvxBind="ItemsSource ReceptionSuppliersList; ItemClick SelectReceptionCommand; Enabled ReceptionSuppliersList.IsValid" />
From what I've tested this just disabled all my list items because there is no such property ReceptionSuppliersList.IsValid ( it's ReceptionSuppliersList[i].IsValid ). How can I achive this.
I also tried to add the Enabled Property on item_supplier like this
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="0dp"
style="#style/ExtendProTheme.ReceptionItem"
local:MvxBind="Enabled IsValid" >
But it still doesn't work.
Any ideas how can i disable some of the item from my list based on a property?
PS :
My items look like this
public string Username { get; set; }
public string DeviceId { get; set; }
public bool IsValid { get; set; }
You could create a custom adapter to handle the enabling/disabling of list items. You would just need to cast the Adapters ItemsSource to your ViewModels
ReceptionSuppliersList type in the IsEnabled override.
public class CustomAdapter : MvxAdapter
{
public CustomAdapter(Context context)
: base(context)
{
}
public CustomAdapter(Context context, IMvxAndroidBindingContext bindingContext)
: base(context, bindingContext)
{
}
protected CustomAdapter(IntPtr javaReference, JniHandleOwnership transfer) :
base(javaReference, transfer)
{
}
public override bool IsEnabled(int position)
{
var items = ItemsSource as List<TestModel>;
return items[position].IsValid;
}
}
Assign adapter to your MvxListView:
var receptionsListView = FindViewById<MvxListView>(Resource.Id.receptionsListView);
receptionsListView.Adapter = new CustomAdapter(this, BindingContext as IMvxAndroidBindingContext);
That's not a real problem-solve, but it should work for your case:
Add gray overlay on the item to make it look like it's not enabled.
Check whether item is enabled on the method handler for SelectReceptionCommand.
private void OnSelectReceptionCommandExecuted(Reception item)
{
if (!reception.IsEnabled)
{
return;
}
// your code here
}

How to implement Recycler View in MvvmCross using MvxRecyclerView

In MvvmCross, I have an android listview within an activity that works.
I read somewhere that changing the listView to a recyclerView is as simple as changing MvxListView to MvxRecyclerView in my layout .axml file. Trying that gives me the following runtime exception:
Android.Views.InflateException: Binary XML file line #1: Binary XML file line #1: Error inflating class Mvx.MvxRecyclerView
Is there anything that I have to do differently in the code behind or view model when using MvxRecyclerView? Below is my code.
Layout files:
Main.axml
<?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="match_parent"
android:layout_height="match_parent">
<Mvx.MvxRecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:id="#+id/words_listview"
local:MvxItemTemplate="#layout/words_listview_row"
local:MvxBind="ItemsSource Words" />
</LinearLayout>
words_listview_row.axml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:p1="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
p1:minWidth="25px"
p1:minHeight="25px"
p1:layout_width="match_parent"
p1:layout_height="match_parent"
p1:id="#+id/relativeLayout1"
p1:background="#FFFFFF">
<TextView
p1:text="Word name"
p1:layout_width="wrap_content"
p1:layout_height="wrap_content"
p1:id="#+id/headingTextView"
p1:width="325dp"
p1:textColor="#000000"
p1:layout_alignParentLeft="true"
p1:textSize="18sp"
p1:paddingLeft="20dp"
p1:paddingTop="15dp"
local:MvxBind="Text Name" />
<TextView
p1:text="Word meaning"
p1:layout_width="match_parent"
p1:layout_height="wrap_content"
p1:layout_below="#id/headingTextView"
p1:id="#+id/detailTextView"
p1:textColor="#8f949a"
p1:layout_alignParentLeft="true"
p1:textSize="16sp"
p1:paddingLeft="20dp"
p1:paddingRight="20dp"
p1:paddingBottom="15dp"
local:MvxBind="Text Meaning" />
</RelativeLayout>
WordViewModel.cs
using MvvmCross.Core.ViewModels;
using VocabBuilder.Core.Models;
using VocabBuilder.Core.Services.Interfaces;
namespace VocabBuilder.Core.ViewModels
{
public class WordViewModel : MvxViewModel
{
private IWordService _wordService;
public MvxObservableCollection<Word> Words { get; set; }
public WordViewModel(IWordService wordService)
{
Words = new MvxObservableCollection<Word>();
_wordService = wordService;
Words.ReplaceWith(_wordService.GetAllWords());
}
}
}
Codebehind/ WordView.cs
using Android.App;
using Android.OS;
using MvvmCross.Droid.Views;
using VocabBuilder.Core.ViewModels;
namespace VocabBuilder.UI.Droid.Views
{
[Activity(Label="Vocab Builder", MainLauncher=true)]
public class WordView : MvxActivity<WordViewModel>
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.Main);
}
}
}
Thanks.
EDIT:
Here is my Setup.cs file:
using Android.Content;
using MvvmCross.Core.ViewModels;
using MvvmCross.Droid.Platform;
namespace VocabBuilder.UI.Droid
{
public class Setup : MvxAndroidSetup
{
public Setup(Context applicationContext) : base(applicationContext)
{
}
protected override IMvxApplication CreateApp()
{
return new Core.App();
}
}
}
OK, I got RecyclerView to work easily after replacing MvxListView with MvxRecyclerView. Not sure what the particular problem with your implementation is but i will give you the tidbits of mine and you can try it all out.
My RecyclerView lives in a Fragment inheriting from MvxFragment - a bit different than yours but should not be the determining factor. Here are some Fragment snippets:
public class ListsFragment : MvxFragment<ListsViewModel>
{
...
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
base.OnCreateView(inflater, container, savedInstanceState);
var view = this.BindingInflate(Resource.Layout.fragment_listsfragment, null);
return view;
}
...
}
Here is the axml for fragment_listsfragment:
<?xml version="1.0" encoding="utf-8"?>
<MvvmCross.Droid.Support.V7.RecyclerView.MvxRecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:MvxItemTemplate="#layout/item_list"
app:MvxBind="ItemsSource Lists; ItemClick ShowListItemsCommand" />
I do see a bit of difference in your ViewModel implementation - mine is like this:
public class ListsViewModel : BaseViewModel
{
...
private readonly IListService _listService;
private ObservableCollection<Models.List> _lists;
public ObservableCollection<Models.List> Lists
{
get { return _lists; }
set
{
_lists = value;
RaisePropertyChanged(() => Lists);
}
}
public MvxCommand<Models.List> ShowListItemsCommand
{
get
{
return new MvxCommand<Models.List>(selectedList =>
{
ShowViewModel<ListItemsViewModel>
(new { listId = selectedList.Id });
});
}
}
...
public override async void Start()
{
base.Start();
await InitializeAsync();
}
protected override async Task InitializeAsync()
{
await _listService.InitializeAsync();
Lists = (await _listService.GetListsAsync())
.ToObservableCollection();
}
}
Hope this helps.

Add a toolbar in a MvxPreferenceFragmentCompat

I'm getting some problems creating a settings page using MvxPreferenceFragmentCompat. I've already shown the preferences view, but I could not include the toolbar in the same page.
I'm using XPlatformMenus sample to create my app.
I've already try many solutions like
attempt 1 or attempt 2, but no success.
Here is my current code:
SettingsFragment.cs
[MvxFragment(typeof(MainViewModel), Resource.Id.content_frame)]
[Register("myapp.droid.fragments.SettingsFragment")]
public class SettingsFragment : BasePreferenceFragment<SettingsViewModel>
{
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
base.showHamburgerMenu = true;
return base.OnCreateView(inflater, container, savedInstanceState);
}
public override void OnCreatePreferences(Bundle savedInstanceState, string rootKey)
{
this.AddPreferencesFromResource(FragmentId);
}
protected override int FragmentId
{
get
{
return Resource.Layout.fragment_settings;
}
}
}
fragment_settings.xml
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<CheckBoxPreference
android:key="pref_sync"
android:title="Title sample"
android:summary="Here is the checkbox summary"
android:defaultValue="true" />
</PreferenceScreen>
BasePreferenceFragment
public abstract class BasePreferenceFragment : MvxPreferenceFragmentCompat
{
protected Toolbar _toolbar;
protected MvxActionBarDrawerToggle _drawerToggle;
/// <summary>
/// If true show the hamburger menu
/// </summary>
protected bool showHamburgerMenu = false;
protected BasePreferenceFragment()
{
RetainInstance = true;
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
var view = base.OnCreateView(inflater, container, savedInstanceState);
_toolbar = view.FindViewById<Toolbar>(Resource.Id.toolbar);
if (_toolbar != null)
{
((MainActivity)Activity).SetSupportActionBar(_toolbar);
if (showHamburgerMenu)
{
((MainActivity)Activity).SupportActionBar.SetDisplayHomeAsUpEnabled(true);
_drawerToggle = new MvxActionBarDrawerToggle(
Activity, // host Activity
((MainActivity)Activity).DrawerLayout, // DrawerLayout object
_toolbar, // nav drawer icon to replace 'Up' caret
Resource.String.drawer_open, // "open drawer" description
Resource.String.drawer_close // "close drawer" description
);
_drawerToggle.DrawerOpened += (object sender, ActionBarDrawerEventArgs e) =>
{
if (Activity != null)
((MainActivity)Activity).HideSoftKeyboard();
};
((MainActivity)Activity).DrawerLayout.AddDrawerListener(_drawerToggle);
}
}
return view;
}
protected abstract int FragmentId { get; }
public override void OnConfigurationChanged(Configuration newConfig)
{
base.OnConfigurationChanged(newConfig);
if (_toolbar != null && null != _drawerToggle)
{
_drawerToggle.OnConfigurationChanged(newConfig);
}
}
public override void OnActivityCreated(Bundle savedInstanceState)
{
base.OnActivityCreated(savedInstanceState);
if (_toolbar != null && null != _drawerToggle)
{
_drawerToggle.SyncState();
}
}
}
public abstract class BasePreferenceFragment<TViewModel> : BasePreferenceFragment where TViewModel : class, IMvxViewModel
{
public new TViewModel ViewModel
{
get { return (TViewModel)base.ViewModel; }
set { base.ViewModel = value; }
}
}
toolbar_actionbar.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:id="#+id/appbar"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
local:popupTheme="#style/ThemeOverlay.AppCompat.Light" />
</android.support.design.widget.AppBarLayout>
fragment_setting_screen.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="#layout/toolbar_actionbar" />
<FrameLayout
android:id="#+id/content_frame"
android:layout_below="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
What am I doing wrong? Any idea?

How to make a Single Row Horizontal ListView/List?

What I want to do is Create a Single Row Listview/List with a image and text is this is what i have right now
I have Changed the numColumns to 1 but how do I change it so it is Horizontal and Scrollable? the image should be one top and the text should be below it?
This is the Layout
<?xml version="1.0" encoding="utf-8"?>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/gridview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:columnWidth="90dp"
android:numColumns="1"
android:verticalSpacing="10dp"
android:horizontalSpacing="10dp"
android:stretchMode="columnWidth"
android:gravity="center"
/>
Single Grid Item
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="match_parent"
android:layout_height="250dp"
android:scaleType="centerCrop"
android:id="#+id/my_image_vieww" />
<TextView
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/my_text_view" />
</LinearLayout>
Main Activity
var gridview = FindViewById<GridView> (Resource.Id.gridview);
gridview.Adapter = new ImageAdapter (this);
gridview.ItemClick += delegate (object sender, AdapterView.ItemClickEventArgs args) {
Toast.MakeText (this, args.Position.ToString (), ToastLength.Short).Show ();
};
public class ImageAdapter : BaseAdapter
{
Context context;
public ImageAdapter (Context c)
{
context = c;
}
public override int Count {
get { return thumbIds.Length; }
}
public override Java.Lang.Object GetItem (int position)
{
return null;
}
public override long GetItemId (int position)
{
return 0;
}
// create a new ImageView for each item referenced by the Adapter
public override View GetView (int position, View convertView, ViewGroup parent)
{
ImageView imageView;
View view;
if (convertView == null) { // if it's not recycled, initialize some attributes
view = LayoutInflater.From(parent.Context).Inflate(Resource.Layout.my_grid_row, parent, false);
} else {
view = convertView;
}
var imageView2 = view.FindViewById<ImageView>(Resource.Id.my_image_vieww);
imageView2.SetImageResource (thumbIds[position]);
var textView = view.FindViewById<TextView>(Resource.Id.my_text_view);
textView.Text = "Text to Display";
return view;
}
}
Please Dont user horizontal list view.
There is predefined android controll to achieve this. View Pager
Sample Code:
var _viewPager = view.FindViewById<ViewPager>(Resource.Id.viewPager);
var _viewPageIndicatorCircle = view.FindViewById<CirclePageIndicator>(Resource.Id.viewPageIndicator);
_viewPager.Adapter = new TestFragmentAdapter(fm);
_viewPageIndicatorCircle.SetViewPager(_viewPager);
public class TestFragmentAdapter : FragmentPagerAdapter
{
public TestFragmentAdapter(Android.Support.V4.App.FragmentManager fm)
: base(fm)
{
}
public override Android.Support.V4.App.Fragment GetItem(int position)
{
return new TestFragment();
}
public override int Count
{
get
{
return 5;
}
}
}
class TestFragment : Android.Support.V4.App.Fragment
{
public TestFragment()
{
}
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
var view = inflater.Inflate(Resource.Layout.MyViewPager , container, false);
return view;
}
}
Viewpager.xml
<android.support.v4.view.ViewPager
android:id="#+id/viewPager"
android:layout_width="fill_parent"
android:layout_height="209.6dp" />
If you have any queries please feel free to ask me

Resources