1
votes

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
    }
4

4 Answers

3
votes

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)

1
votes

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));
}
1
votes

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
    }
0
votes

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.