1

I had found several (not too many) different approaches for grouping cells in Xamarin+mvvmcross. I've tried couple of them, but i'm getting a problem: when my real service returns updated collection (with a small delay), binding is not really working and list remains empty. If to run fake service, which returns result instantly, list is filled with data.

I've tried several approaches, but no one pushed me forward, so i'm not sure if any code is necessary. Just asking if there's a sample/hints for the modern implementation of the grouping.

EDIT: here's a code sample. Current version is based on Stuart's answer: Unable to bind to ios table section cells in mvvmcross, child cells bind as expected

ViewModel:

    public override async void OnShow()
    {
        var calendarList = await DataService.GetListAsync();

        CalendarList = new List<Model>(calendarList.OrderBy(a => a.Date));
    }

So, viewmodel gets a list of Models, order it by Date and set it to CalendarList. CalendarList is just a List which throws notifications (new is doing this job).

View, initializing:

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();

        var source = new TableSource(CalendarList);

        this.AddBindings(new Dictionary<object, string>
        {
            {source, "ItemsSource CalendarList" }
        });

        CalendarList.Source = source;
        CalendarList.ReloadData();
    }

View, TableSource

    public class TableSource : MvxSimpleTableViewSource
    {
        private static readonly NSString CalendarCellIdentifier = new NSString("CalendarCell");
        private List<IGrouping<DateTime, Model>> GroupedCalendarList;

        public TableSource(UITableView calendarView) : base(calendarView, "CalendarCell", "CalendarCell")
        { 
        }


        public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
        {
            var foo = GroupedCalendarList[indexPath.Section].ToList();
            var item = foo[indexPath.Row];

            var cell = GetOrCreateCellFor(tableView, indexPath, item);

            var bindable = cell as IMvxDataConsumer;
            if (bindable != null)
                bindable.DataContext = item;

            return cell;
        }

        public override nint RowsInSection(UITableView tableview, nint section)
        {
            var numberRows = GroupedCalendarList[(int)section].Count();
            return numberRows;
        }

        public override UIView GetViewForHeader(UITableView tableView, nint section)
        {
            var label = new UILabel();
            label.Text = GroupedCalendarList[(int)section].FirstOrDefault().ScheduledStart.Value.Date.ToString();
            label.TextAlignment = UITextAlignment.Center;

            return label;
        }

        public override nint NumberOfSections(UITableView tableView)
        {
            return GroupedCalendarList.Count;
        }

        public override void ReloadTableData()
        {
            if (ItemsSource == null) return;
            var calendarList = new List<Model>(ItemsSource as List<ActivityModel>);

            List<IGrouping<DateTime, Model>> groupedCalendarList = calendarList.GroupBy(cl => cl.ScheduledStart.Value.Date).ToList();

            if (ItemsSource != null)
                GroupedCalendarList = new List<IGrouping<DateTime, Model>>(groupedCalendarList);
        }
    }

One of the symptoms is that ReloadTableData() is not called when VM is updating the list.

EDIT 2: well, i've tried to use ObservableCollection and i saw that ReloadTableData is called, but UI is still empty. If anyone can provide a working sample - that'd be great.

Updated VM:

    public override async void OnShow()
    {
        var calendarList = await DataService.GetListAsync();

        CalendarList.Clear();
        foreach (var item in calendarList.OrderBy(a => a.ScheduledStart.Value.Date))
        {
            CalendarList.Add(item);  // Lets bruteforce and send notification on each add. In real life it's better to use extension ObservableCollection.AddRange().
        }
    }

Updated View:

public override void ReloadTableData()
        {
            if (ItemsSource == null) return;
            var calendarList = ItemsSource as IEnumerable<Model>;

            var groupedCalendarList = calendarList.GroupBy(cl => cl.ScheduledStart.Value.Date).ToList();

            GroupedCalendarList = new List<IGrouping<DateTime, Model>>(groupedCalendarList);

            Mvx.Trace("Trying to add new item " + calendarList.Count());
        }

In that case, output is full of

Diagnostic: 9.54 Trying to add new item 77

But the UI list is still empty.

EDIT3: If to replace VM.OnShow() with adding Task.Run().Wait(); to the VM's ctor (so it would delay View.ViewDidLoad() until data is loaded) - then list shows correctly.

public ViewModel()
    {
        Task.Run(async () =>
        {
            var calendarList = await DataService.GetListAsync();
            CalendarList = new ObservableCollection<ActivityModel>(calendarList);
        }).Wait(); // This guy is called before View.ViewDidLoad() and grouped list is shown properly
    }
Community
  • 1
  • 1
Vitalii Vasylenko
  • 4,776
  • 5
  • 40
  • 64

4 Answers4

3

There could be a number of issues here.

  1. I wouldn't recommend binding like you have. Instead create a binding set and use:

var set = this.CreateBindingSet<TClass, TViewModel>(); set.Bind(source).To(vm => vm.Collection); set.Apply();

  1. Use an observable collection and then when you have finished adding to your observable collection call: RaisePropertyChanged(() => Collection);

  2. To make it easy to group the data I override ItemsSource to push the data into a dictionary of values and an array of keys to make it much easier to work out the sections.

  3. Since you just want to supply text just override TitleForHeader: public override string TitleForHeader(UITableView tableView, nint section)

A Springham
  • 2,038
  • 1
  • 13
  • 10
  • hi, 1 - yea, tried that, does not affect the issue, but you're right, i'll replace it with proper strong-typed binding. 2 - Alerts sounds good, have to try that. 3 - yes, i had played with that, no real difference, but makes sense to rearrange the data. I was actually considering to put it into doctionary with key as groupname and value as the list of items. 4 - right, will check it. Thanks for suggestions! – Vitalii Vasylenko Aug 11 '16 at 09:33
  • Sorry the Alerts was a typo (I copied code). I have edited it. You just call that on your collection list. If you override ItemsSource it was at least tell you if the property is being raised to the TableViewSource. – A Springham Aug 11 '16 at 09:35
  • Ah... i thought, Alerts is something like "force refresh the whole view". – Vitalii Vasylenko Aug 11 '16 at 09:49
  • I've tried OC, but unfortunately UI is stil empty. Did i forget to override something? – Vitalii Vasylenko Aug 14 '16 at 17:27
  • Did you override ItemsSource and see if it is being called? – A Springham Aug 15 '16 at 08:49
1

It's hard to give good advice if you don't share some code. But from reading your question I think that you're using a List<T> instead of an ObservableCollection<T>. The UI needs to know when the collection updates, thus it needs INotifyCollectionChanged and INotifyPropertyChanged implementation to do so. ObservableCollection implements these interfaces.


ViewModel:

Make sure that CalendarList is of type ObservableCollection<Model>. You won't need the method ReloadTable() after this change. In class MvxSimpleTableViewSource his base MvxTableViewSource ReloadTableData is called in the setter of ItemsSource as you can see in the following link:

https://github.com/MvvmCross/MvvmCross/blob/4.0/MvvmCross/Binding/iOS/Views/MvxTableViewSource.cs#L54

public override async void OnShow()
{
    var calendarList = await DataService.GetListAsync();

    CalendarList = new ObservableCollection<Model>(calendarList.OrderBy(a => a.Date));
}
Mark Verkiel
  • 1,229
  • 10
  • 22
  • Hi, yea,probably i'll try to pick some code or even a sample project. I'm just creating new list when i'm getting the data, and that should be actually enough: new list raises notification. The problem starts exactly when i'm trying grouping. – Vitalii Vasylenko Aug 10 '16 at 17:09
  • Added some samples. – Vitalii Vasylenko Aug 10 '16 at 18:08
  • Could you try changing your List to ObservableCollection? It will result in not having to caal ReloadTableData() when your ViewModel updates the collection. I can post an example tomorrow. – Mark Verkiel Aug 10 '16 at 21:41
  • Unfortunately, no luck with OC. Care to provide a sample? – Vitalii Vasylenko Aug 14 '16 at 17:27
  • Thanks for the sample. Yep, i wrote exactly as you described, but the problem is that i need to call base.ReloadTableData() from inside of override ReloadTableData() - that fixed the isssue. Otherwise, ReloadTableData() was called after each List.Add() in my Edit2, but no changes were shown on the UI. Also, i believe that calling `new ObservableCollection()` is very much the same as calling `new List()`. – Vitalii Vasylenko Aug 14 '16 at 19:49
  • PS: not sure where exactly you added the sample, i noted it right now, after finding the solution. Anyway, thanks! – Vitalii Vasylenko Aug 14 '16 at 19:49
1

I'm blind! Thanks to @nmilcoff from MvvmCross Slack channel for highlighting the problem. I forgot to call base.ReloadTableData().

And of course thanks for Pilatus and Springham for good hints.

Here's the solution:

    public override void ReloadTableData()
    {
        if (ItemsSource == null) return;
        var calendarList = new List<Model>(ItemsSource as List<ActivityModel>);

        List<IGrouping<DateTime, Model>> groupedCalendarList = calendarList.GroupBy(cl => cl.ScheduledStart.Value.Date).ToList();

        if (ItemsSource != null)
            GroupedCalendarList = new List<IGrouping<DateTime, Model>>(groupedCalendarList);

        base.ReloadTableData(); // Here we are
    }
danday74
  • 52,471
  • 49
  • 232
  • 283
Vitalii Vasylenko
  • 4,776
  • 5
  • 40
  • 64
0

Seems like you update your data list as soon as you post the data request.

It's incorrect, this is a async operation, you should update your data when the http request finished.

There a sample to finish async data request:

string strURL = "https://api.bitcoinaverage.com/ticker/";
        MyHTTPRequestManager.Instance.GetDataFromUrl (strURL,(string dataStr)=>{
            Console.WriteLine("Getting data succeed");
            Console.WriteLine("The dataStr = "+dataStr);
            //update your dataList here

            InvokeOnMainThread(delegate {
                //Update your tableView or collectionView here, all UI stuff must be invoke on Main thread  
            });
        });

And this is the MyHTTPRequestManager.cs:

public class MyHTTPRequestManager
{
    public delegate void GettingDataCallback(string dataStr);

    private static MyHTTPRequestManager instance = null;
    public static MyHTTPRequestManager Instance{
        get{
            if(null == instance)
                instance = new MyHTTPRequestManager();
            return instance;
        }
    }

    public void GetDataFromUrl(string strURL,GettingDataCallback callback)
    {
        Console.WriteLine ("Begin request data.");
        System.Net.HttpWebRequest request;
        request = (System.Net.HttpWebRequest)WebRequest.Create(strURL);
        System.Net.HttpWebResponse response;
        response = (System.Net.HttpWebResponse)request.GetResponse();
        System.IO.StreamReader myreader = new System.IO.StreamReader(response.GetResponseStream(), Encoding.UTF8);
        string responseText = myreader.ReadToEnd();
        myreader.Close();
        Console.WriteLine ("Getting succeed, invoke callback.");
        callback.Invoke (responseText);
    }
}

And this is the result screen shot:

Result pic

Hope it can help you.

Alanc Liu
  • 1,294
  • 10
  • 15
  • Hi, unfortunately, not really. When data is loaded, i'm creating a new list, which throws notification so ui should be updated. It works fine for simple plain lists, but if to talk about grouping - the problems appears. – Vitalii Vasylenko Aug 10 '16 at 17:12
  • Added some samples. – Vitalii Vasylenko Aug 10 '16 at 18:08
  • One more thing: using callbacks it a quite outdated practice, it may lead to spaghetti-code, especially if you have multi-level-callbacks. Take a look at async calls, they are making life much easier. Check the code i have provided (ViewModel part) or search it out on SO: http://stackoverflow.com/questions/14455293/how-and-when-to-use-async-and-await – Vitalii Vasylenko Aug 10 '16 at 19:10
  • Thanks for you suggestion, I will check it. – Alanc Liu Aug 11 '16 at 14:03