MvvmCross iOS UITableView doesn't update on Property Changed - ios

My table view does not update whenever its source property is changed. The code is as follows:
ViewController:
public override void ViewDidLoad()
{
base.ViewDidLoad();
View.BackgroundColor = UIColor.White;
var table = new UITableView(new RectangleF(0, 80, Device.Width, Device.Height - 80));
Add(table);
var source = new MvxStandardTableViewSource(table, "TitleText SessionInfo");
table.Source = source;
var set = this.CreateBindingSet<JoinSessionViewController, JoinSessionViewModel>();
set.Bind(source).To(vm => vm.AvailableServers);
set.Apply();
table.ReloadData();
}
ViewModel:
private readonly INetworkSessionClient _client;
public JoinSessionViewModel(INetworkSessionClient client)
{
_client = client;
_client.ServerFound += ClientOnServerFound;
_client.BeginSearchingForServers();
}
private void ClientOnServerFound(object sender, ServerFoundEventArgs serverFoundEventArgs)
{
if (AvailableServers.Any(s => s.Identifier == serverFoundEventArgs.ServerInfo.Identifier))
return;
AvailableServers.Add(serverFoundEventArgs.ServerInfo);
RaisePropertyChanged(() => AvailableServers);
}
private List<ServerInfo> _availableServers;
public List<ServerInfo> AvailableServers
{
get { return _availableServers; }
set { _availableServers = value; RaisePropertyChanged(() => AvailableServers); }
}

This was a tricky one. The culprit is this line in MvxTableViewSource:
https://github.com/MvvmCross/MvvmCross/blob/v3.1/Cirrious/Cirrious.MvvmCross.Binding.Touch/Views/MvxTableViewSource.cs#L56
public virtual IEnumerable ItemsSource
{
get { return _itemsSource; }
set
{
if (_itemsSource == value) // **** This one ****
return;
if (_subscription != null)
{
_subscription.Dispose();
_subscription = null;
}
_itemsSource = value;
var collectionChanged = _itemsSource as INotifyCollectionChanged;
if (collectionChanged != null)
{
_subscription = collectionChanged.WeakSubscribe(CollectionChangedOnCollectionChanged);
}
ReloadTableData();
}
}
Since I was only adding to the list and not setting it to a new list, the setter was early returning and not firing ReloadTableData(). The fix was to use an ObservableCollection (or anything else that implements INotifyCollectionChanged) instead of a List.
private void ClientOnServerFound(object sender, ServerFoundEventArgs serverFoundEventArgs)
{
if (AvailableServers.Any(s => s.Identifier == serverFoundEventArgs.ServerInfo.Identifier))
return;
// CollectionChanged event is not automatically martialed to UI thread
InvokeOnMainThread(() => AvailableServers.Add(serverFoundEventArgs.ServerInfo));
}
private ObservableCollection<ServerInfo> _availableServers;
public ObservableCollection<ServerInfo> AvailableServers
{
get { return _availableServers; }
set { _availableServers = value; RaisePropertyChanged(() => AvailableServers); }
}
Note that because of my particular case, the ServerFound event is being invoked on a separate thread. That means the INotifyCollectionChanged.CollectionChanged event will also be invoked on a separate thread, so the action of adding the server to the list has to be martialed onto the main thread with InvokeOnMainThread() so that the UI will properly update.
It would be nice if CollectionChanged events would automatically be handled on the UI thread, similar to how RaisePropertyChanged() works.

Related

Update custom listviewAdapter

I wanne add new data to my listviewAdapter, i tried it with clearing all the data before it and notifying that there is now data but it doesn't seem to work. I followed the Solution (stackoverflow) but it doesn't work.
Does anyone have an idea why?
public class ReportsListViewAdapter : BaseAdapter<IMobileReport>, IFilterable
{
internal List<IMobileReport> originalData;
internal List<IMobileReport> reports;
private Context context;
public override IMobileReport this[int position] => reports[position];
public ReportsListViewAdapter(Context context, IEnumerable<IMobileReport> reports)
{
this.reports = reports.OrderBy(report => report.StudyDate).ToList();
this.context = context;
Filter = new ReportsFilter(this);
}
public override int Count => this.reports.Count;
public Filter Filter { get; private set; }
public override long GetItemId(int position)
{
return position;
}
public void updateReportsList(List<MobileReport> newlist)
{
reports.AddRange(newlist);
this.NotifyDataSetChanged();
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
View row = convertView;
if(row == null)
{
row = LayoutInflater.From(context).Inflate(Resource.Layout.listView_reports_row, null, false);
}
var txtName = row.FindViewById<TextView>(Resource.Id.txtName);
txtName.Text = reports[position].Student.Name;
var txtFirstName = row.FindViewById<TextView>(Resource.Id.txtFirstName);
txtFirstName.Text = reports[position].Student.FirstName;
var txtSource = row.FindViewById<TextView>(Resource.Id.txtSource);
txtSource.Text = reports[position].Source;
var txtCritical = row.FindViewById<TextView>(Resource.Id.txtCritical);
txtSource.Text = reports[position].Critical.ToString();
return row;
}
}
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
reports = new List<IMobileReport>();
//Init();
_reportsHubConnector = new ReportsHubConnector();
#pragma warning disable 4014 // We cannot await this task here because the signature of the inherited method is void
Task.Factory.StartNew(async () =>
{
await _reportsHubConnector.StartConnection();
await _reportsHubConnector.SendT();
}, TaskCreationOptions.PreferFairness);
#pragma warning restore 4014
Console.WriteLine("HomeActivity: OnCreate");
SetContentView(Resource.Layout.activity_reports);
SupportActionBar.SetDisplayShowTitleEnabled(false);
SupportActionBar.SetDisplayHomeAsUpEnabled(false);
SupportActionBar.SetDisplayShowHomeEnabled(true);
WireUpElements();
listView = FindViewById<ListView>(Resource.Id.reports);
ReportsListViewAdapter adapter = new ReportsListViewAdapter(this, reports);
listView.Adapter = adapter;
searchView = FindViewById<SearchView>(Resource.Id.searchView1);
searchView.QueryTextChange += this.Filter;
listView.ItemClick += ItemClicked;
criticalButton = FindViewById<LinearLayout>(Resource.Id.AuthenticatorButton);
criticalButton.Click += criticalClicked;
_reportsHubConnector.ReportsRecieved += (reports) =>
{
adapter.updateReportsList(reports);
};
}
When i debug slowly the GetView does get triggered, maybe this is a clue to why its not being called when i don't debug or go over the code quick.
This.RunOnUiThread is never called but update is.
_reportsHubConnector.ReportsRecieved += (tmpReports) =>
{
adapter.updateReportsList(tmpReports);
this.RunOnUiThread(() =>
{
criticalButton.SetBackgroundColor(Android.Graphics.Color.Red);
});
};
i tried it with clearing all the data before it and notifying that there is now data but it doesn't seem to work.
From shared code , not seeing clear method , you can add reports.Clear() to check whether it works .
public void updateReportsList(List<MobileReport> newlist)
{
reports.Clear();
reports.AddRange(newlist);
this.NotifyDataSetChanged();
}
If not works , need to check whehter added newlist is the correct data format.
========================Update========================
In OnCreate method , where _reportsHubConnector.ReportsRecieved call update method modify as follow :
_reportsHubConnector.ReportsRecieved += (tmpReports) =>
{
adapter.updateReportsList(tmpReports);
};
Change reports argument to tmpReports to avoid mixing with the original data reports .
Therefore , there is another common way to update data of adapter as follow :
_reportsHubConnector.ReportsRecieved += (reports) =>
{
List<IMobileReport> tmp = new List<IMobileReport>();
tmp = reports ; // use a tmp list data for updating , not using original list data
adapter.updateReportsList(tmp);
};
============================Update===============================
From my sample project , I find a interesting phenomenon and that maybe the problem.
Here I will share my custom adapter HomeScreenAdapter :
public class HomeScreenAdapter : BaseAdapter<TableItem> {
List<TableItem> items;
Activity context;
public HomeScreenAdapter(Activity context, List<TableItem> items)
: base()
{
this.context = context;
this.items = new List<TableItem>();
this.items.AddRange(items);
//this.items = items;
}
public override long GetItemId(int position)
{
return position;
}
public override TableItem this[int position]
{
get { return items[position]; }
}
public override int Count
{
get { return items.Count; }
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
var item = items[position];
View view = convertView;
if (view == null) // no view to re-use, create new
view = context.LayoutInflater.Inflate(Resource.Layout.CustomView, null);
view.FindViewById<TextView>(Resource.Id.Text1).Text = item.Heading;
view.FindViewById<TextView>(Resource.Id.Text2).Text = item.SubHeading+" items";
view.FindViewById<ImageView>(Resource.Id.Image).SetImageResource(item.ImageResourceId);
return view;
}
public void UpdateListView(List<TableItem> newTableItem)
{
items.Clear();
items.AddRange(newTableItem);
NotifyDataSetChanged();
}
}
You will see in the Constructor of HomeScreenAdapter , I commented this line of code this.items = items; . The reault that it will work:
However , if I use this.items = items; to replace this.items.AddRange(items);, it will update nothing even can not show anything in ListView .
public HomeScreenAdapter(Activity context, List<TableItem> items)
: base()
{
this.context = context;
this.items = new List<TableItem>();
//this.items.AddRange(items);
this.items = items;
}
The effect :
The possible reason :
If the equal sign is used here, the pointer address of the items will change. When the data is updated, it cannot point to the original data source, so the update cannot be successful.
Therefore , here Constructor of your code can modify as follow to check whehter it works :
public ReportsListViewAdapter(Context context, IEnumerable<IMobileReport> reports)
{
//this.reports = reports.OrderBy(report => report.StudyDate).ToList();
this.reports = new List<IMobileReport>();
this.reports.AddRange(reports.OrderBy(report => report.StudyDate).ToList());
this.context = context;
Filter = new ReportsFilter(this);
}
Related : Here is the sample project link .

xamarin ios Tableview is not getting Reloaded when i use MvxSimpleTableViewSource

My table view does not update whenever its source property is changed. The code is as follows:
public override void ViewDidLoad()
{
base.ViewDidLoad();
viewmodel = this.ViewModel as ListViewModel;
viewmodel.PropertyChanged += HandlePropertyChangedEventHandler;;
var source = new MvxSimpleTableViewSource( TableView, LaborCell.Key, LaborCell.Key);
TableView.Source = source;
var set = this.CreateBindingSet<ListView, ListViewModel>();
set.Bind(source).To(vm => vm.LaborTransactions);
set.Apply();
TableView.ReloadData();
}
ViewModel:
public class ListViewModel :MaxRawBaseViewModel
{
public ListViewModel():base()
{
LoadLaborTransactions();
}
private Collection<LaborTransaction> _laborTransactions;
public Collection<LaborTransaction> LaborTransactions
{
get { return _laborTransactions; }
}
public void LoadLaborTransactions()
{
_laborTransactions = DataService.GetLaborTransactions(somenumber);
RaisePropertyChanged(() => LaborTransactions);
}
}
When ever the change in Transactions am calling the tablview.reolad() on propertychanged method. but it is not reloading my tableview
void HandlePropertyChangedEventHandler(object sender, System.ComponentModel.PropertyChangedEventArgs e){
if (e.PropertyName.Equals("LaborTransactions"))
{
TableView.ReloadData();
}
}
Collection<T> does not implement INotifyPropertyChanged. You can verify that in the docs here. You need to change your LaborTransactions property to a collection type that implements INotifyPropertyChanged like ObservableCollection<T> and MvxObservableCollection<T>. You can see that ObservableCollection<T> implements INotifyPropertyChanged here
Change your LaborTransactions as such:
private ObservableCollection<LaborTransaction> _laborTransactions;
public ObservableCollection<LaborTransaction> LaborTransactions
{
get { return _laborTransactions; }
set {
return _laborTransactions;
RaisePropertyChanged(() => LaborTransactions);
}
}

MVVMCROSS Ios Binding ShowViewModel

I've got problem with data-binding in mvvmcross after doing the navigation in the model by calling the showviewmodel-method. On the android side it works.
So the Problem is, that the navigation itself is working but I don't get any data from the model.
Navigation in the model:
ShowViewModel<TeamEventDetailsViewModel>(new { eventID = item.ID });
ViewModel which containts the Data:
public class TeamEventDetailsViewModel
: EventDetailsViewModel
{
public TeamEventModel CurrentEvent
{
get { return MyCurrentEvent as TeamEventModel; }
set
{
MyCurrentEvent = value;
RaisePropertyChanged(() => CurrentEvent);
TickerModel.Comments = value.Comments;
RaisePropertyChanged(() => TickerModel);
LineupModel.Team1Players = value.Team1Players;
LineupModel.Team2Players = value.Team2Players;
RaisePropertyChanged(() => LineupModel);
}
}
private EventDetailsLineupViewModel _lineupModel = new EventDetailsLineupViewModel();
public EventDetailsLineupViewModel LineupModel
{
get { return _lineupModel; }
set { _lineupModel = value; RaisePropertyChanged(() => LineupModel); }
}
public TeamEventDetailsViewModel()
{
EventToken = MvxMessenger.Subscribe<EventUpdateMessage>(OnEventUpdateMessage);
}
private void OnEventUpdateMessage(EventUpdateMessage eventUpdate)
{
if (MyCurrentEvent != null && eventUpdate.Event.ID == MyCurrentEvent.ID)
{
var updatedEvent = (TeamEventModel)eventUpdate.Event;
var myEvent = CurrentEvent;
if(updatedEvent.Score!=null)
myEvent.Score = updatedEvent.Score;
if (updatedEvent.Team1Players != null)
myEvent.Team1Players = updatedEvent.Team1Players;
if (updatedEvent.Team2Players != null)
myEvent.Team2Players = updatedEvent.Team2Players;
CurrentEvent = myEvent;
}
}
protected override void Update(EventModel eventdetails)
{
CurrentEvent = (TeamEventModel) eventdetails;
}
private string _teststring = "success";
public string Teststring
{
get { return _teststring; }
set
{
_teststring = value;
RaisePropertyChanged(()=>_teststring);
}
}
}
As you can see at the bottom I implemented a teststring to prove functionality.
Binding in the View:
public class TeamEventDetailsView : MvxViewController
{
public UILabel TestLabel = new UILabel();
public TeamEventDetailsViewModel TeamEventDetailsViewModel
{
get { return (TeamEventDetailsViewModel)base.ViewModel; }
set { base.ViewModel = value; }
}
public override void ViewDidLoad()
{
View.AddSubview(TestLabel);
this.CreateBinding(TestLabel).To<TeamEventDetailsViewModel>(vm => vm.Teststring).Apply();
TestLabel.BackgroundColor = UIColor.Orange;
}
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
TestLabel.Frame=new RectangleF(0,20,View.Frame.Width,80);
}
}
So I repeat, the navigation itself works but the data from model doesn't get shown on the view.
If I create the ViewModel manually in the View then the binding works also, but in my Situation I can't do that because the Data is pulled depending on the generated Data from the ViewModel which calls the navigation-proceed.
Manual ViewModel:
TeamEventDetailsViewModel = new TeamEventDetailsViewModel();
TeamEventDetailsViewModel.Init(9816);
As I can tell I did exactly the same as Stuard does in his Tutorial:
https://www.youtube.com/watch?v=cbdPDZmuHk8
Does anyone has an advice for me?
Thanks.
MvvmCross does the ViewModel create in base.ViewDidLoad() - if you add that call to your ViewDidLoad override then everything should work ok

MVVMCROSS Binding TimeSyncProblem

I've got a special problem with binding data to an itemsource of an mvxtablieviewsource.
I'm trying to generate a list of favorites, which are generated in the core by clicking on a different tablview.
Normally I get the databinding working like this: (Just basic structure)
controller:
var source = new MySource(TableView);
this.AddBindings(new Dictionary<object, string>
{
{source, "ItemsSource Favs"}
});
Source:
private List<FavModel> _favs;
public override IEnumerable ItemsSource
{
get { return _favs; }
set
{
_favs = (List<FavModel>)value;
ReloadTableData();
}
}
protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
{
var cell = new AdFavCell();
cell.TextLabel.Text = ((FavModel)item).Display;
return cell;
}
Normally it works really great but no in this case where i generate the data by reacting on users touch, I've got this strange failure;
When I set a breakpoint in the setter of the ItemsSource, and wait for a while then it works correctly.
When I run without a breakpoint the tableview keeps empty.
I also figured out that if I insert a manually pause in the setter then it works too:
Setter with pause:
public override IEnumerable ItemsSource
{
get { return _mydata; }
set
{
_favs = (List<FavModel>)value;
ReloadTableData();
Task.Delay(1000).Wait();
}
}
I also tried to do a delaybinding, but it didn't work.
Have anyone an idea where the problem is?
Edit:
Here some additional Information:
How the Data is generated:
I've got a tableview with content and depending on a longclick on a cell, I create a popumenu where you can add your favorites.
Detecting the longclick:
protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
{
MvxTableViewCell cell = null;
if (item is SoccerEventListModel)
{
cell = tableView.DequeueReusableCell(this.CellIdentifier) as SoccerEvent;
if (cell == null)
{
cell = new SoccerEvent((SoccerEventListModel)item);
cell.AddGestureRecognizer(new UILongPressGestureRecognizer((e) =>
{
if (e.State == UIGestureRecognizerState.Began)
{
var command = ItemLongClickCommand;
if (command != null)
command.Execute(item);
}
}));
return cell;
}
}
}
Binding the Longclick to the core:
EventListViewModel.EventFavViewCallbackEvent += EventListViewModel_EventFavViewCallbackEvent;
void EventListViewModel_EventFavViewCallbackEvent(EventModel e)
{
var StoreFav = new EventFavoritesView { ViewModel = new EventFavoritesViewModel { ID = e.ID } };
View.Add(StoreFav.View);
}
Depending on the ID of the cell, it creates the list of the favorites by sending a request to our server.
Update:
private long _id;
public long ID
{
get { return _id; }
set { _id = value; RaisePropertyChanged(() => ID); Update(); }
}
When the data is received a RaisePropertyChanged() should make the view to reload its content.
private List<FavModel> _favs;
public List<FavModel> Favs
{
get { return _favs; }
set { _favs = value; RaisePropertyChanged(() => Favs); }
}
ViewModel:
public class EventFavoritesViewModel : MvxViewModel
{
private readonly EventFavoritesService _eventFavoriteService;
private readonly UserFavoritesService _userFavoriteService;
private long _id;
public long ID
{
get { return _id; }
set { _id = value; RaisePropertyChanged(() => ID); Update(); }
}
private string _title;
public string Title
{
get { return _title; }
set { _title = value; RaisePropertyChanged(() => Title); }
}
private List<FavModel> _favs;
public List<FavModel> Favs
{
get { return _favs; }
set { _favs = value; RaisePropertyChanged(() => Favs); }
}
private MvxCommand<FavModel> _itemSelectedCommand;
public System.Windows.Input.ICommand ItemSelectedCommand
{
get
{
_itemSelectedCommand = _itemSelectedCommand ?? new MvxCommand<FavModel>(ToggleFav);
return _itemSelectedCommand;
}
}
public void Init(long eventID)
{
MvxTrace.Trace("We get the details", Logger.Errorlevel.Debug);
ID = eventID;
}
public EventFavoritesViewModel()
{
_eventFavoriteService = new EventFavoritesService(UpdateEventFav);
_userFavoriteService = new UserFavoritesService(UpdateUserFav);
}
private void UpdateUserFav(Fav[] favlist)
{
MvxMessenger.Publish(new UserFavUpdateMessage(this, favlist));
}
private void Update()
{
Favs = _eventFavoriteService.GetFavforEvent(ID).MapToFavs();
}
private void UpdateEventFav(Fav[] favlist)
{
Favs = favlist.MapToFavs();
}
private void ToggleFav(FavModel item)
{
MvxTrace.Trace("Got Item: " + item.Display);
item.NewSubscription = !item.NewSubscription;
}
private IMvxMessenger MvxMessenger
{
get
{
return Mvx.Resolve<IMvxMessenger>();
}
}
public void SaveFavs()
{
foreach (var fav in Favs)
{
if (fav.AlreadySubscribed != fav.NewSubscription)
{
if (fav.NewSubscription)
_userFavoriteService.PutToUserFavorites(fav.MapToFav());
else
_userFavoriteService.DeleteFromUserFavorites(fav.MapToFav());
}
}
}
}
I hope this is enough information, otherwise just tell me.:-)
Thanks for any help.

How to bind Command to ios table section header click?

How can I bind Command to recieve taps on sections of my Table? I am using MvxViewController with custom TableSource, but it seems that I cant add bindings to my VM when creating UIView for sections.
Here is my ViewModel:
public class TestViewModel : MvxViewModel
{
private ObservableCollection<string> _sections;
public ObservableCollection<string> Sections
{
get { return _sections; }
set { _sections = value; RaisePropertyChanged(() => Sections); }
}
private MvxCommand _sectionTappedCommand;
public ICommand SectionTappedCommand
{
get
{
_sectionTappedCommand = _sectionTappedCommand ?? new MvxCommand(DoSectionTappedCommand);
return _sectionTappedCommand;
}
}
private void DoSectionTappedCommand()
{
//I want this command somehow to be called when user taps section header
Debug.WriteLine("Section tapped!");
}
}
My view:
[Register("TestView")]
public class OrderView : MvxViewController
{
public override void ViewDidLoad()
{
View = new UIView() { BackgroundColor = UIColor.White };
base.ViewDidLoad();
var table = new UITableView(new RectangleF(0, 20, 320, 660));
Add(table);
var source = new TestTableSource(table);
table.Source = source;
var set = this.CreateBindingSet<OrderView, OrderViewModel>();
// I think here I need to write something like:
// set.Bind(source).For(s => s.Section.TapAction).To(vm => vm.SectionTappedCommand);**
set.Bind(source).For(s => s.ItemsSource).To(vm => vm.Sections).OneWay();
set.Apply();
}
}
Table source:
public class TestTableSource : MvxBaseTableViewSource
{
// all needed overrides implemented
private IList<OrderGuest> _sections;
public IList<OrderGuest> ItemsSource
{
get
{
return _sections;
}
set
{
_sections = value;
ReloadTableData();
}
}
public override UIView GetViewForHeader(UITableView tableView, int section)
{
// Do I need to add bindings here?
var view = new OrderGuestSectionHeader(OrderGuestSectionHeader(tableView, section), () => {
Debug.WriteLine("selected " + section.ToString());
});
return view;
}
public override int NumberOfSections(UITableView tableView)
{
if (_sections == null)
return 0;
return _sections.Count;
}
public override string[] SectionIndexTitles(UITableView tableView)
{
if (_sections == null)
return null;
return _sections.Select(x => x.Name).ToArray();
}
}
Subclassed UIView for section header:
public sealed class OrderGuestSectionHeader : UIView
{
private UIButton SectionButton;
public Action TapAction;
public OrderGuestSectionHeader(string header, Action tapped)
{
Frame = new RectangleF(0, 0, 320, 20);
BackgroundColor = UIColor.Blue;
SectionButton = new UIButton(this.Frame);
SectionButton.TouchUpInside += SectionButton_TouchUpInside;
SectionButton.Title(header);
TapAction = tapped;
Add(SectionButton);
}
private void SectionButton_TouchUpInside(object sender, EventArgs e)
{
TapAction();
}
}
There are a few ways you could achieve this effect.
For just your current requirements, the easiest way to achieve it would be to add just a single command to the TestTableSource and to have that command passed on to your section header inside the Action handler.
public class TestTableSource : MvxBaseTableViewSource
{
public ICommand FooCommand { get; set; }
// existing code
public override UIView GetViewForHeader(UITableView tableView, int section)
{
var view = new OrderGuestSectionHeader(OrderGuestSectionHeader(tableView, section), () => {
Debug.WriteLine("selected " + section.ToString());
if (FooCommand != null) FooCommand.Execute(null);
});
return view;
}
}
This Command can then be bound to the ViewModel by adding the OrderView binding:
set.Bind(source)
.For(s => s.FooCommand)
.To(vm => vm.SectionTappedCommand)
.OneWay();
If you wanted to go further - if you wanted to do a more complicated binding - then you could actually set up a full binding DataContext for the Header View. The easiest way to do this would be to inherit from MvxView. I won't go into the full detail of this here - instead for an introduction to MvxView, see the N+1 video - http://slodge.blogspot.co.uk/2013/06/n32-truth-about-viewmodels-starring.html with sample source code in https://github.com/slodge/NPlus1DaysOfMvvmCross/tree/master/N-32-ViewModels

Resources