Mvvmcross Changing Viewmodel Within a Mvxbindablelistview

MVVMCross changing ViewModel within a MvxBindableListView

Your analysis is definitely correct about where the click event is trying to bind.

There are two approaches I generally take:

  1. Use ItemClick on the List
  2. Continuing using Click but do some redirection on the ViewModel side.

So...1

The Main Menu in the tutorial has a ViewModel a bit like:

public class MainMenuViewModel
: MvxViewModel
{
public List<T> Items { get; set; }

public IMvxCommand ShowItemCommand
{
get
{
return new MvxRelayCommand<T>((item) => /* do action with item */ );
}
}
}

This is used in axml as:

<Mvx.MvxBindableListView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res/Tutorial.UI.Droid"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
local:MvxBind="{'ItemsSource':{'Path':'Items'},'ItemClick':{'Path':'ShowItemCommand'}}"
local:MvxItemTemplate="@layout/listitem_viewmodel"
/>

This approach can only be done for ItemClick on the whole list item - not on individual subviews within the list items.


Or...2

Since we don't have any RelativeSource binding instructions in mvx, this type of redirection can be done in the ViewModel/Model code.

This can be done by presenting a behaviour-enabled wrapper of the Model object rather than the Model object itself - e.g. using a List<ActiveArticle>:

public ActiveArticle
{
Article _article;
ArticleViewModel _parent;

public WrappedArticle(Article article, ArticleViewModel parent)
{
/* assignment */
}

public IMvxCommand TheCommand { get { return MvxRelayCommand(() -> _parent.DoStuff(_article)); } }

public Article TheArticle { get { return _article; } }
}

Your axml would then have to use bindings like:

    <TextView            ...
local:MvxBind="{'Text':{'Path':'TheArticle.Label'}}" />

and

    <ImageButton
...
local:MvxBind="{'Click':{'Path':'TheCommand.MyTest'}}" />

One example of this approach is the Conference sample which uses WithCommand

However... please note that when using WithCommand<T> we discovered a memory leak - basically the GarbageCollection refused to collect the embedded MvxRelayCommand - which is why WithCommand<T> is IDisposable and why BaseSessionListViewModel clears the list and disposes the WithCommand elements when views are detached.


Update after comment:

If your data list is large - and your data is fixed (your articles are models without PropertyChanged) and you don't want to incur the overhead of creating a large List<WrappedArticle> then one way around this might be to use a WrappingList<T> class.

This is very similar to the approach taken in Microsoft code - e.g. in virtualizing lists in WP7/Silverlight - http://shawnoster.com/blog/post/Improving-ListBox-Performance-in-Silverlight-for-Windows-Phone-7-Data-Virtualization.aspx

For your articles this might be:

public class ArticleViewModel: MvxViewModel
{
public WrappingList<Article> Articles;

// normal members...
}

public class Article
{
public string Label { get; set; }
public string Remark { get; set; }
}

public class WrappingList<T> : IList<WrappingList<T>.Wrapped>
{
public class Wrapped
{
public IMvxCommand Command1 { get; set; }
public IMvxCommand Command2 { get; set; }
public IMvxCommand Command3 { get; set; }
public IMvxCommand Command4 { get; set; }
public T TheItem { get; set; }
}

private readonly List<T> _realList;
private readonly Action<T>[] _realAction1;
private readonly Action<T>[] _realAction2;
private readonly Action<T>[] _realAction3;
private readonly Action<T>[] _realAction4;

public WrappingList(List<T> realList, Action<T> realAction)
{
_realList = realList;
_realAction = realAction;
}

private Wrapped Wrap(T item)
{
return new Wrapped()
{
Command1 = new MvxRelayCommand(() => _realAction1(item)),
Command2 = new MvxRelayCommand(() => _realAction2(item)),
Command3 = new MvxRelayCommand(() => _realAction3(item)),
Command4 = new MvxRelayCommand(() => _realAction4(item)),
TheItem = item
};
}

#region Implementation of Key required methods

public int Count { get { return _realList.Count; } }

public Wrapped this[int index]
{
get { return Wrap(_realList[index]); }
set { throw new NotImplementedException(); }
}

#endregion

#region NonImplementation of other methods

public IEnumerator<Wrapped> GetEnumerator()
{
throw new NotImplementedException();
}

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}

public void Add(Wrapped item)
{
throw new NotImplementedException();
}

public void Clear()
{
throw new NotImplementedException();
}

public bool Contains(Wrapped item)
{
throw new NotImplementedException();
}

public void CopyTo(Wrapped[] array, int arrayIndex)
{
throw new NotImplementedException();
}

public bool Remove(Wrapped item)
{
throw new NotImplementedException();
}

public bool IsReadOnly { get; private set; }

#endregion

#region Implementation of IList<DateFilter>

public int IndexOf(Wrapped item)
{
throw new NotImplementedException();
}

public void Insert(int index, Wrapped item)
{
throw new NotImplementedException();
}

public void RemoveAt(int index)
{
throw new NotImplementedException();
}

#endregion
}

MvvmCross Android - Alternative to RelativeSource binding for button command

See the second option in the answer in MVVMCross changing ViewModel within a MvxBindableListView - this covers one way to do this.

Using that approach you'd expose a list of objects like:

public class Wrapped
{
public ICommand GoThruCommand { get; set; }
public ICommand OpenCommand { get; set; }
public string Name { get; set; }
}

and you'd use an axml list template with bound controls like:

<TextView
...
local:MvxBind="{'Text':{'Path':'Name'}}" />

<Button
...
local:MvxBind="{'Click':{'Path':'GoCommand'}}" />

<Button
...
local:MvxBind="{'Click':{'Path':'ThruCommand'}}" />

if you've got suggestions/requests for relative source in mvx, please add them to https://github.com/slodge/MvvmCross/issues/35

Binding button click in ListView template MvvMCross

I've found the solution. The problem was the click binding. You should only refer to the action in the wrapper class and not both. Here are my wrapperclass & listview itemtemplate.

ItemTemplate:

 <LinearLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
android:minWidth="25px"
android:minHeight="25px">
<Button
android:layout_width="wrap_content"
android:layout_height="70dip"
android:layout_alignParentRight="true"
android:layout_marginTop="20dip"
android:layout_marginRight="20dip"
android:layout_gravity="right|center_vertical"
android:text="Bestel"
local:MvxBind="Click OrderClick" />
</LinearLayout>

WrapperClass:

public class MenuItemWrap
{
MenuItem _mnuItem;
ListPresentationViewModel _parent;

public MenuItemWrap (MenuItem item, ListPresentationViewModel parent)
{
_mnuItem = item;
_parent = parent;
}

public IMvxCommand OrderClick {
get {
return new MvxCommand (() => _parent.btnClick (_mnuItem));
}
}

public MenuItem Item{ get { return _mnuItem; } }
}

My ViewModel:

public class ListPresentationViewModel: MvxViewModel 
{
private readonly ISQLService _sqlSvc;

public ListPresentationViewModel (ISQLService sqlService)
{
_sqlSvc = sqlService;
MenuCollection = WrapConverter.ConvertToWrapperClass (_sqlSvc.MenuItemGetAll(), this);
}

private int _catId;
public int CategorieId {
get{ return _catId;}
set{
_catId = value;
ChangeMenuCollection ();
}
}

private void ChangeMenuCollection()
{
MenuCollection = WrapConverter.ConvertToWrapperClass (_sqlSvc.MenuItemByCategorie (_catId), this);
}

private List<MenuItemWrap> _menuCollection = new List<MenuItemWrap> ();
public List<MenuItemWrap> MenuCollection {
get{ return _menuCollection;}
set {
_menuCollection = value;
RaisePropertyChanged (() => MenuCollection);
}
}

private IMvxCommand _orderBtnClick;

public IMvxCommand OrderBtnClick {
get {
_orderBtnClick = _orderBtnClick ?? new MvxCommand<MenuItem> (btnClick);
return _orderBtnClick;
}
}

public void btnClick (MenuItem item)
{
//Do Something
}
}

MVVMCross Get SelectedItem from a MvxBindableListView

The lack of SelectedItem in Droid was identified as an issue last week during preparation for Daniel's talk at Build.

To workaround it, there were a couple of quick answers:

1 There is SelectedItemPosition you can use for binding - this is an int

2 You can use a Click ICommand/IMvxCommand binding instead of using SelectedItem - in your example, this would be the same axml but

public IMvxCommand ShowItemCommand
{
get
{
return new MvxRelayCommand<Address>(address => DoShowContact(address));
}
}

To be clear this Click option above is what I would use.


If SelectedItem really is needed...

Then for a complete answer, Daniel and I prototyped a new binding. This binding was registered using:

        registry.RegisterFactory(new MvxCustomBindingFactory<MvxBindableListView>("SelectedItem", adapterView => new MvxAdapterViewSelectedItemTargetBinding(adapterView)));

and contained the logic:

using System;
using Android.Widget;
using Cirrious.MvvmCross.Binding.Droid.Views;
using Cirrious.MvvmCross.Binding.Interfaces;
using Cirrious.MvvmCross.Interfaces.Platform.Diagnostics;

namespace Cirrious.MvvmCross.Binding.Droid.Target
{
#warning This needs to be redone for all adapterviews not just list view!
#warning The use of ItemClick instead of ItemSelected needs to be reinvestigated here!
public class MvxAdapterViewSelectedItemTargetBinding : MvxBaseAndroidTargetBinding
{
private readonly MvxBindableListView _view;
private object _currentValue;

public MvxAdapterViewSelectedItemTargetBinding(MvxBindableListView view)
{
_view = view;
((ListView)_view).ItemClick += OnItemClick;
}

private void OnItemClick(object sender, AdapterView.ItemClickEventArgs itemClickEventArgs)
{
var container = (_view.GetItemAtPosition(itemClickEventArgs.Position) as MvxJavaContainer);
if (container == null)
{
MvxBindingTrace.Trace(MvxTraceLevel.Warning, "Missing MvxJavaContainer in MvxAdapterViewSelectedItemTargetBinding");
return;
}
var newValue = container.Object;
if (!newValue.Equals(_currentValue))
{
_currentValue = newValue;
FireValueChanged(newValue);
}
}

public override void SetValue(object value)
{
#warning Sort out Equals test here
if (value != null && value != _currentValue)
{
var index = _view.Adapter.GetPosition(value);
if (index < 0)
{
MvxBindingTrace.Trace(MvxTraceLevel.Warning, "Value not found for spinner {0}", value.ToString());
return;
}
_currentValue = value;
_view.SetSelection(index);
}
}

public override MvxBindingMode DefaultMode
{
get { return MvxBindingMode.TwoWay; }
}

public override Type TargetType
{
get { return typeof(object); }
}

protected override void Dispose(bool isDisposing)
{
if (isDisposing)
{
((ListView)_view).ItemClick -= OnItemClick;
}
base.Dispose(isDisposing);
}
}
}

To test this worked, I used the Tutorial PullToRefresh code adapted using:

<Mvx.MvxBindableListView         android:id="@android:id/list"         android:layout_width="fill_parent"
android:layout_height="fill_parent"
local:MvxBind="{'ItemsSource':{'Path':'Emails'},'ItemClick':{'Path':'ShowItemCommand'},'SelectedItem':{'Path':'TheSelectedEmail'}}"
local:MvxItemTemplate="@layout/listitem_email"
/>

and:

    public class SimpleEmail
{
public string From { get; set; }
public string Header { get; set; }
public string Message { get; set; }
}

private ObservableCollection<SimpleEmail> _emails;
public ObservableCollection<SimpleEmail> Emails
{
get { return _emails; }
private set { _emails = value; RaisePropertyChanged(() => Emails); }
}

private SimpleEmail _email;
public SimpleEmail TheSelectedEmail
{
get { return _email; }
set
{
_email = value;
MvxTrace.Trace(MvxTraceLevel.Error, "HELLO {0} ", value == null ? "null" : value.From);
}
}

One thing to be careful about in all this work is that a listview selected item in Android is slightly different to a listbox selected item in Silverlight/wp - e.g. it can be quite hard to get a listview in android to highlight the current selection and it can be quite hard to get the listview to generate selection changed events.


Note: I've logged an issue on Droid SelectedItem to https://github.com/slodge/MvvmCross/issues/52 - I'll make sure the binding is added to the core library in the near future

Mvvm cross binding data context

I'm not sure if that's what you are asking, but to make a binding of the person's name to the listitem textview:

local:MvxBind="Text Name"

And instead of using a List you should use an ObservableCollection

ObservableCollection<Person> _personCollection;       
ObservableCollection<Person> PersonCollection
{
get { return _personCollection; }
set
{
_personCollection = value;
RaisePropertyChanged (() => PersonCollection);
}

}

For the checkbox i would add a field to the Person class such as IsSelected and bind it to the checkbox:

 local:MvxBind="Checked IsSelected"

Can't bind a MvxBindableListView in TwoWay Mode

The way data-binding works is through an interface called INotifyPropertyChanged

What happens in this interface is that the ViewModel sends the View a message whenever a property changes - e.g.

    FirePropertyChanged("TestList");

With a list, this doesn't help if the contents of the list itself change - e.g. when the list has an item added or removed.


To solve this, the .Net Mvvm implementation includes another interface INotifyCollectionChanged.

A collection - such as a list - can implement INotifyCollectionChanged in order to let the View know when the contents of the collection change.

For example, the collection might fire events containing hints such as:

  • everything has changed - NotifyCollectionChangedAction.Reset
  • an item has been added - NotifyCollectionChangedAction.Add
  • an item has been removed - NotifyCollectionChangedAction.Remove
  • ...

There's a short introduction into this interface about 12:30 into the MvvmCross Xaminar http://www.youtube.com/watch?v=jdiu_dH3z5k

Xaminar


To use this interface for a small in-memory list - e.g. less than 1000 'small' objects - all you have to do is to change your List<T> for an ObservableCollection<T> - the ObservableCollection is a class from the core .Net libraries (from Microsoft or Mono) and it will fire the correct events when you Add/Remove list items.

You can see the source for the Mono ObservableCollection implementation in: https://github.com/mosa/Mono-Class-Libraries/blob/master/mcs/class/System/System.Collections.ObjectModel/ObservableCollection.cs - it is worth taking some time to look at this implementation so that you can understand a bit more about how Mvvm works with INotifyCollectionChanged.

If you use the ObservableCollection class, then your code will become:

    private ObservableCollection<MyType> _testList;
public ObservableCollection<MyType> TestList
{
get { return _testList; }
set
{
_testList = value;
FirePropertyChanged("TestList");
// in vNext use RaisePropertyChanged(() => TestList);
}
}

with:

 <Mvx.MvxBindableListView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
local:MvxBind="{'ItemsSource':{'Path':'TestList'}}"
local:MvxItemTemplate="@layout/my_item_layout" />

Note:

  • that the binding is OneWay - this means that binding is still only going from ViewModel to View - there are no updates going from View to ViewModel.
  • that ObservableCollection is designed to be single-threaded - so make sure all changes to the collection are done on the UI thread - not on a worker thread. If you need to, you can marshall work back onto the UI thread using InvokeOnMainThread(() => { /* do work here */ }) in a ViewModel.
  • that in Android, the way lists work (through the base AdapterView) means that every time you call any update on the ObservableCollection then the UI List will ignore the action hint (Add, Remove, etc) - it will treat every change as a Reset and this will cause the entire list to redraw.

For larger collections - where you don't want all the items in memory at the same time - you may need to implement some data-store backed list yourself.

There is a brief example of one simple sqlite data-backed store in https://github.com/slodge/MvvmCross/blob/vnext/Sample%20-%20SimpleDialogBinding/SimpleDroidSql.Core/DatabaseBackedObservableCollection.cs

This virtualizing of collection data is common in WP and WPF apps - e.g. see questions and answers like Is listbox virtualized by default in WP7 Mango?

Does MVVMCross have a way to change the selector when clicking ImageButton on ListView?

Partly, this feels like this is just 'UI eye candy' - and so falls within the domain of 'View concern' - and so it isn't something that mvvmcross normally tries to make cross platform.

However... I think there is a way.

If the command handler within the ViewModel, also sets a CurrentSelectedPosition integer on the ViewModel, then each UI can bind the SelectedItemPosition from ViewModel to each list in each ui - and this should cause the UI to natively update the selection.

I think that would work... But on Android it would need some binding :

public class MvxAdapterViewSelectedItemPositionTargetB-inging : MvxBaseAndroidTargetBinding
{
private readonly AdapterView _adapterView;

public MvxAdapterViewSelectedItemPositionTargetBinging(AdapterView adapterView)
{
_adapterView = adapterView;
_adapterView.ItemSelected += AdapterViewOnItemSelected;
}

public override void SetValue(object value)
{
_adapterView.SetSelection((int)value);
}

private void AdapterViewOnItemSelected(object sender, AdapterView.ItemSelectedEventArgs itemSelectedEventArgs)
{
FireValueChanged(itemSelectedEventArgs.Position);
}

public override MvxBindingMode DefaultMode
{
get
{
return MvxBindingMode.TwoWay;
}
}

public override Type TargetType
{
get
{
return typeof(Int32);
}
}

protected override void Dispose(bool isDisposing)
{
if (isDisposing)
{
if (_adapterView != null)
{
_adapterView.ItemSelected -= AdapterViewOnItemSelected;
}
}
base.Dispose(isDisposing);
}
}

registered using:

        registry.RegisterFactory(new MvxCustomBindingFactory<AdapterView>("SelectedItemPosition", adapterView => new MvxAdapterViewSelectedItemPositionTargetBinging(adapterView)));

and bound to the UI in axml e.g. as:

<Mvx.MvxBindableListView
android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
local:MvxBind="{'ItemsSource':{'Path':'Emails'},'SelectedItemPosition':{'Path':'CurrentSelectedPosition'}}"
local:MvxItemTemplate="@layout/listitem_email"
/>

I tested this idea using an email list where the ViewModel list items were:

    public class SimpleEmail
{
public EmailViewModel Parent { get; set; }
public string From { get; set; }
public string Header { get; set; }
public string Message { get; set; }
public ICommand Command1
{
get
{
return new MvxRelayCommand(() => Parent.CurrentSelectedPosition = Parent.Emails.IndexOf(this));
}
}
}
  • but obviously this is just a demo...

Note: I'm using selected position rather than selected object in the code above - because I know the lists you are using are very long!


If you wanted to consider a different approach to your android-only code, then I think you can do that by inheriting from Mvx.MvxBindableListView (and possibly the list item too) and using those classes to update the selection in a possibly less painful way.

MvvmCross and ListBox / ComboBox binding in WinRT

I think this is a general issue with Mvvm on WinRT

For some unknown reason, Microsoft didn't include Behaviors in WinRT. (There are theories like the fact they ran out of time, they didn't want to because of Expression Blend changes, etc...)

There have been quite a few blog posts and articles about how to work around this - most of it centered around Joost van Schaik's excellent library:

  • http://dotnetbyexample.blogspot.co.uk/2012/07/a-winrt-behavior-to-mimic-eventtocommand.html
  • http://blog.tattoocoder.com/2012/08/getting-started-w-windows-8-mvvm-light.html

I've not done this myself - I tend not to use SelectionChanged but instead to put ICommands within each ListItem instead. If you get Joost's library working I'd love to see a sample :)

Passing on variables from ViewModel to another View (MVVMCross)

MVVMCross is very convention based - and it works on the idea of passing messages between ViewModels wherever possible.

If you navigate to a ViewModel using:

KeyValuePair<string,string> kvpAct1 = new KeyValuePair<string, string>("short", ".countertest5");

public IMvxCommand BeckhoffActuator1
{
get
{
return new MvxRelayCommand<Type>((type) => this.RequestNavigate<Beckhoff.BeckhoffActuatorViewModel>(kvpAct1));
}
}

then you should be able to pick that up in the BeckhoffActuatorViewModel using the constructor:

public class BeckhoffActuatorViewModel : MvxViewModel
{
public BeckhoffActuatorViewModel(string short)
{
ShortValue = short;
}

private string _shortValue;
public string ShortValue
{
get
{
return _shortValue;
}
set
{
_shortValue = value;
FirePropertyChanged("ShortValue");
}
}
}

And your views can then access ViewModel.ShortValue (for iOS this can be done after base.ViewDidLoad(), for Android after OnCreate() and for WP7 after OnNavigatedTo)

For an example of this, take a look at the TwitterSearch example:

  • https://github.com/slodge/MvvmCrossTwitterSearch

This has a HomeViewModel which calls navigate using:

    private void DoSearch()
{
RequestNavigate<TwitterViewModel>(new { searchTerm = SearchText });
}

and a TwitterViewModel which receives the searchTerm using the constructor:

    public TwitterViewModel(string searchTerm)
{
StartSearch(searchTerm);
}

Please note that only strings are allowed in this message passing at present - but you can always serialise your own objects using JSON.Net - or you can extend the framework - it's open source.

Please note that only strings, ints, doubles and bools are allowed in this constructor parameter passing at present - this is due to serialisation requirements for Xaml Urls and for Android Intents. If you want to experiment with navigation using your own custom serialised objects, then please see http://slodge.blogspot.co.uk/2013/01/navigating-between-viewmodels-by-more.html.

Also, note that if you want to use the anonymous object navigation (RequestNavigate<TwitterViewModel>(new { searchTerm = SearchText });) then you will need to make sure that an InternalsVisibleTo attribute is set - see https://github.com/slodge/MvvmCrossTwitterSearch/blob/master/TwitterSearch.Core/Properties/AssemblyInfo.cs:

[assembly: InternalsVisibleTo("Cirrious.MvvmCross")]

Further... not for the faint-hearted... and this isn't "good mvvm code"... but if you really want/need to access the MvxShowViewModelRequest data inside an Android activity, then you can extract it from the incoming Intent - there's an Extras string containing the request (see the deserialisation in CreateViewModelFromIntent in https://github.com/slodge/MvvmCross/blob/master/Cirrious/Cirrious.MvvmCross/Android/Views/MvxAndroidViewsContainer.cs)



Related Topics



Leave a reply



Submit