ScrollView not updating when elements are changed on Android

WammyWammy USMember ✭✭

I have a ContentPage, with a ScrollView that has a StackLayout with a couple of nested StackLayouts.

Essentially:

public class Widgets : ContentPage
{
    StackLayout layout;
    public Widgets()
    {
        layout = new StackLayout()
        {
            VerticalOptions = LayoutOptions.FillAndExpand,
            Orientation = StackOrientation.Vertical
        };

        var Widget = new Wiget();
        layout.Children.Add(Widget.Render());
        //Widget.Render() returns a StackLayout with some other UI elements, it also loads some items from the web and adds them to the stacklayout. 

        var Widget2 = new Wiget2(); //Same idea...
        layout.Children.Add(Widget2.Render());

        Content = new ScrollView()
        {
            HorizontalOptions = LayoutOptions.FillAndExpand,
            VerticalOptions = LayoutOptions.FillAndExpand,
            Content = layout
        };
    }
}

When the Widget class finishes loading the web items, and adds them to the stacklayout, the new elements are not rendered, in fact any change to the UI does not get rendered. The widgets display a Loading icon while the items are loading, this is displayed properly and the loading icon disappears when loading is complete, but the new elements do not show up.

This appears this when running on Android, the ScrollView and related Widget elements work properly on iOS. Admittedly I can only try Android on the emulator so not sure if it is an issue with it. I am using the built-in Android_Accelerated_Nougat with API version 25. (7.1.1)

Best Answers

Answers

  • JohnHardmanJohnHardman GBUniversity ✭✭✭✭✭

    @Wammy - Does your Wiget.Render method call any async methods by any chance (or things that Render calls then call async methods)?

  • WammyWammy USMember ✭✭

    @JohnHardman Widget constructor sets up a BackgroundWorker that retrieves the data, the rest of the UI elements are added/changed on .RunWorkerCompleted()

  • WammyWammy USMember ✭✭

    @JohnHardman Sure thing, I've been refactoring this to see if perhaps it is my implementation.

    I've now instead derived from ContentView and setup what Render() returned to be .Content.

    This is the base DashboardWidget class:

    public class DashboardWidget : ContentView, Interfaces.IDashboardWidget
    {
        public event EventHandler<DashboardWidgetMessage> DisplayMessage;
        public event EventHandler<OpenPageArgs> OpenPage;
        public event EventHandler<EventArgs> BeginLoading;
        public event EventHandler<EventArgs> FinishedLoading;
    
    
        protected void OnBeginLoading()
        {
            BeginLoading?.Invoke(this, new EventArgs());
        }
    
        protected void OnFinishedLoading()
        {
            FinishedLoading?.Invoke(this, new EventArgs());
        }
    
        protected void OnDisplayMessage(DashboardWidgetMessage args)
        {
            DisplayMessage?.Invoke(this, args);
        }
    
        protected void OnOpenPage(Page p, bool asModal = false)
        {
            OpenPage?.Invoke(this, new OpenPageArgs() { Page = p, AsModal = asModal });
        }
    
        protected void OnErrorLoading(App.Network.HTTPErrorEventArgs args)
        {
            OnDisplayMessage(new DashboardWidgetMessage() { Title = "Error: " + args.Code.ToString(), Message = args.Message });
        }
    }
    

    Here it is implemented, I've removed non-essential code.

    public class DashboardWidget : Views.DashboardWidget
    {
        SortedList<string, string> arguments = new SortedList<string, string>();
    
        int page = 1;
        int rowCount = 5;
        string sort_by = "due";
        string sort_order = "asc";
        string URL = "";
    
        StackLayout taskList;
    
        public DashboardWidget()
        {
    
            taskList = new StackLayout();
            Content = new StackLayout()
            {
                Orientation = StackOrientation.Vertical,
                Children = {
                        new TitleView ("Tasks"),
                        taskList
                    }
            };
    
            loadTasks();
    
    
        }
    
        void loadTasks()
        {
    
            taskList.Children.Clear();
    
            OnBeginLoading();
    
            taskList.Children.Add(new StackLayout()
            {
                VerticalOptions = LayoutOptions.Center,
                Children = {
                        new ActivityIndicator() {
                            IsRunning = true
                        }
                    }
            });
    
            var worker = new BackgroundWorker();
    
            worker.DoWork += (object sender, DoWorkEventArgs e) => {
                var options = (SortedList<string, object>)e.Argument;
                try
                {
                    string args = "discard_view_option=1&orderBy=" + options["orderBy"] + "&order=" + options["order"] + "&page=" + options["page"] + "&count=" + options["count"] + "&real_values=1";
                    using (var req = new App.Network.PostRequest(URL, args))
                    {
                        req.DataReceived += (s, d) => {
                            e.Result = d.Data;
                        };
                        req.HTTPError += (s, d) => {
                            OnErrorLoading(d);
                        };
                        req.SendRequest();
                    }
                }
                catch (Exception ex)
                {
    #if DEBUG
                    Console.WriteLine(ex.Message);
                    throw;
    #endif
                }
    
            };
    
            worker.RunWorkerCompleted += (object sender, RunWorkerCompletedEventArgs e) => {
    
                if (e.Error != null)
                {
                    OnErrorLoading(new App.Network.HTTPErrorEventArgs
                    {
                        Code = 0,
                        Message = e.Error.Message
                    });
                }
                else
                {
                    if (!string.IsNullOrEmpty((string)e.Result))
                    {
    
                        List<Models.Task> result = JsonConvert.DeserializeObject<List<Models.Task>>((string)e.Result);
                        taskList.Children.Clear();
                        if (result.Count > 0)
                        {
                            foreach (Models.Task task in result)
                            {
                                var taskView = new Views.Task.ListItem();
                                taskView.BindingContext = task;
                                var taskTap = new TapGestureRecognizer();
                                taskTap.Tapped += (object s, EventArgs er) => {
    
                                    var page = new Pages.Object.View(task);
                                    page.Disappearing += (se, en) => {
                                        loadTasks();
                                    };
                                    OnOpenPage(page);
                                };
                                taskView.GestureRecognizers.Add(taskTap);
                                taskList.Children.Add(taskView);
                            }
                            var viewMore = new ContentView()
                            {
                                HeightRequest = 60,
                                VerticalOptions = LayoutOptions.Fill,
                                Content = new StackLayout()
                                {
                                    HorizontalOptions = LayoutOptions.Center,
                                    VerticalOptions = LayoutOptions.End,
                                    Children = {
                                            new Label() {
                                                Text = "View All Tasks >"
                                            }
                                        }
                                }
                            };
                            var moreTap = new TapGestureRecognizer();
                            moreTap.Tapped += (object s, EventArgs er) => {
                                var page = new Pages.Task.List();
                                OnOpenPage(page);
                            };
                            viewMore.GestureRecognizers.Add(moreTap);
                            taskList.Children.Add(viewMore);
                        }
                        else
                        {
                            taskList.Children.Add(new StackLayout()
                            {
                                VerticalOptions = LayoutOptions.Center,
                                Children = {
                                        new Label() {
                                            Text = "No Tasks Found"
                                        }
                                    }
                            });
                        }
    
                    }
                }
                OnFinishedLoading();
    
    
            };
    
            var listOptions = new SortedList<string, object>();
            listOptions.Add("orderBy", sort_by);
            listOptions.Add("order", sort_order);
            listOptions.Add("page", page);
            listOptions.Add("count", rowCount);
    
            worker.RunWorkerAsync(listOptions);
    
        }
    
    
    }
    
  • JohnHardmanJohnHardman GBUniversity ✭✭✭✭✭

    @Wammy - I need to dash, so I'll take a proper look tomorrow. However, without seeing all of the code that this is dependent upon, my hunch is that when data is received (or other events happen), that is not on the UI thread? If so, you will need to use Device.BeginInvokeOnMainThread to modify the UI from those event handlers.

  • WammyWammy USMember ✭✭

    @JohnHardman I tried wrapping the calls to taskList.Children.Add() in BeginInvokeOnMainThread() and still the same result.

    Keep in mind this works properly on iOS, so not sure if it has to do with the UI thread.

  • JohnHardmanJohnHardman GBUniversity ✭✭✭✭✭
    edited October 18

    @Wammy - Can you get hold of a physical Android device to test this on? My suspicion is still that this is thread-related, whether updating the UI from a non-UI thread, or a race condition. However, it could also be an emulator issue, so I suggest trying to identify if that is the case.

    Also, in terms of Android vs. iOS, different platforms behave differently when threading code is incorrect for some reason. When you run this in a debugger, are you seeing anything in the output window regarding marshalling of data, using objects on a thread other than the one they were created on, etc?

    As an aside, is there a particular reason that you are using BackgroundWorker rather than Task.Run ?

  • WammyWammy USMember ✭✭

    @JohnHardman I can see about getting my hands on an actual device. Should I look for any model in particular? We do have a few android installs and so far I have not received any complaints regarding this, so it may be an emulator issue.

    I am not getting anything in the output window with the terms you mentioned.

    I just rebuilt the solution and guess what....it is working properly.

    I did keep the changes made last night wrapping all the calls in BeginInvokeOnMainThread:

    Device.BeginInvokeOnMainThread(() => {
            taskList.Children.Add(taskView);
       });
    

    Not sure why it was not working yesterday when I first tried it.

    As to the BackgroundWorker, My previous C# experience is from .Net 2.0 so I'm used to using it.

  • WammyWammy USMember ✭✭

    @JohnHardman Sure thing, I've been refactoring this to see if perhaps it is my implementation.

    I've now instead derived from ContentView and setup what Render() returned to be .Content.

    This is the base DashboardWidget class:

    public class DashboardWidget : ContentView, Interfaces.IDashboardWidget
    {
        public event EventHandler<DashboardWidgetMessage> DisplayMessage;
        public event EventHandler<OpenPageArgs> OpenPage;
        public event EventHandler<EventArgs> BeginLoading;
        public event EventHandler<EventArgs> FinishedLoading;
    
    
        protected void OnBeginLoading()
        {
            BeginLoading?.Invoke(this, new EventArgs());
        }
    
        protected void OnFinishedLoading()
        {
            FinishedLoading?.Invoke(this, new EventArgs());
        }
    
        protected void OnDisplayMessage(DashboardWidgetMessage args)
        {
            DisplayMessage?.Invoke(this, args);
        }
    
        protected void OnOpenPage(Page p, bool asModal = false)
        {
            OpenPage?.Invoke(this, new OpenPageArgs() { Page = p, AsModal = asModal });
        }
    
        protected void OnErrorLoading(App.Network.HTTPErrorEventArgs args)
        {
            OnDisplayMessage(new DashboardWidgetMessage() { Title = "Error: " + args.Code.ToString(), Message = args.Message });
        }
    }
    

    Here it is implemented, I've removed non-essential code.

    public class DashboardWidget : Views.DashboardWidget
    {
        SortedList<string, string> arguments = new SortedList<string, string>();
    
        int page = 1;
        int rowCount = 5;
        string sort_by = "due";
        string sort_order = "asc";
        string URL = "";
    
        StackLayout taskList;
    
        public DashboardWidget()
        {
    
            taskList = new StackLayout();
            Content = new StackLayout()
            {
                Orientation = StackOrientation.Vertical,
                Children = {
                        new TitleView ("Tasks"),
                        taskList
                    }
            };
    
            loadTasks();
    
    
        }
    
        void loadTasks()
        {
    
            taskList.Children.Clear();
    
            OnBeginLoading();
    
            taskList.Children.Add(new StackLayout()
            {
                VerticalOptions = LayoutOptions.Center,
                Children = {
                        new ActivityIndicator() {
                            IsRunning = true
                        }
                    }
            });
    
            var worker = new BackgroundWorker();
    
            worker.DoWork += (object sender, DoWorkEventArgs e) => {
                var options = (SortedList<string, object>)e.Argument;
                try
                {
                    string args = "discard_view_option=1&orderBy=" + options["orderBy"] + "&order=" + options["order"] + "&page=" + options["page"] + "&count=" + options["count"] + "&real_values=1";
                    using (var req = new App.Network.PostRequest(URL, args))
                    {
                        req.DataReceived += (s, d) => {
                            e.Result = d.Data;
                        };
                        req.HTTPError += (s, d) => {
                            OnErrorLoading(d);
                        };
                        req.SendRequest();
                    }
                }
                catch (Exception ex)
                {
    #if DEBUG
                    Console.WriteLine(ex.Message);
                    throw;
    #endif
                }
    
            };
    
            worker.RunWorkerCompleted += (object sender, RunWorkerCompletedEventArgs e) => {
    
                if (e.Error != null)
                {
                    OnErrorLoading(new App.Network.HTTPErrorEventArgs
                    {
                        Code = 0,
                        Message = e.Error.Message
                    });
                }
                else
                {
                    if (!string.IsNullOrEmpty((string)e.Result))
                    {
    
                        List<Models.Task> result = JsonConvert.DeserializeObject<List<Models.Task>>((string)e.Result);
                        taskList.Children.Clear();
                        if (result.Count > 0)
                        {
                            foreach (Models.Task task in result)
                            {
                                var taskView = new Views.Task.ListItem();
                                taskView.BindingContext = task;
                                var taskTap = new TapGestureRecognizer();
                                taskTap.Tapped += (object s, EventArgs er) => {
    
                                    var page = new Pages.Object.View(task);
                                    page.Disappearing += (se, en) => {
                                        loadTasks();
                                    };
                                    OnOpenPage(page);
                                };
                                taskView.GestureRecognizers.Add(taskTap);
                                taskList.Children.Add(taskView);
                            }
                            var viewMore = new ContentView()
                            {
                                HeightRequest = 60,
                                VerticalOptions = LayoutOptions.Fill,
                                Content = new StackLayout()
                                {
                                    HorizontalOptions = LayoutOptions.Center,
                                    VerticalOptions = LayoutOptions.End,
                                    Children = {
                                            new Label() {
                                                Text = "View All Tasks >"
                                            }
                                        }
                                }
                            };
                            var moreTap = new TapGestureRecognizer();
                            moreTap.Tapped += (object s, EventArgs er) => {
                                var page = new Pages.Task.List();
                                OnOpenPage(page);
                            };
                            viewMore.GestureRecognizers.Add(moreTap);
                            taskList.Children.Add(viewMore);
                        }
                        else
                        {
                            taskList.Children.Add(new StackLayout()
                            {
                                VerticalOptions = LayoutOptions.Center,
                                Children = {
                                        new Label() {
                                            Text = "No Tasks Found"
                                        }
                                    }
                            });
                        }
    
                    }
                }
                OnFinishedLoading();
    
    
            };
    
            var listOptions = new SortedList<string, object>();
            listOptions.Add("orderBy", sort_by);
            listOptions.Add("order", sort_order);
            listOptions.Add("page", page);
            listOptions.Add("count", rowCount);
    
            worker.RunWorkerAsync(listOptions);
    
        }
    
    
    }
    
  • WammyWammy USMember ✭✭

    @JohnHardman Sure thing, I've been refactoring this to see if perhaps it is my implementation.

    I've now instead derived from ContentView and setup what Render() returned to be .Content.

    This is the base DashboardWidget class:

    public class DashboardWidget : ContentView, Interfaces.IDashboardWidget
    {
        public event EventHandler<DashboardWidgetMessage> DisplayMessage;
        public event EventHandler<OpenPageArgs> OpenPage;
        public event EventHandler<EventArgs> BeginLoading;
        public event EventHandler<EventArgs> FinishedLoading;
    
    
        protected void OnBeginLoading()
        {
            BeginLoading?.Invoke(this, new EventArgs());
        }
    
        protected void OnFinishedLoading()
        {
            FinishedLoading?.Invoke(this, new EventArgs());
        }
    
        protected void OnDisplayMessage(DashboardWidgetMessage args)
        {
            DisplayMessage?.Invoke(this, args);
        }
    
        protected void OnOpenPage(Page p, bool asModal = false)
        {
            OpenPage?.Invoke(this, new OpenPageArgs() { Page = p, AsModal = asModal });
        }
    
        protected void OnErrorLoading(App.Network.HTTPErrorEventArgs args)
        {
            OnDisplayMessage(new DashboardWidgetMessage() { Title = "Error: " + args.Code.ToString(), Message = args.Message });
        }
    }
    

    Here it is implemented, I've removed non-essential code.

    public class DashboardWidget : Views.DashboardWidget
    {
        SortedList<string, string> arguments = new SortedList<string, string>();
    
        int page = 1;
        int rowCount = 5;
        string sort_by = "due";
        string sort_order = "asc";
        string URL = "";
    
        StackLayout taskList;
    
        public DashboardWidget()
        {
    
            taskList = new StackLayout();
            Content = new StackLayout()
            {
                Orientation = StackOrientation.Vertical,
                Children = {
                        new TitleView ("Tasks"),
                        taskList
                    }
            };
    
            loadTasks();
    
    
        }
    
        void loadTasks()
        {
    
            taskList.Children.Clear();
    
            OnBeginLoading();
    
            taskList.Children.Add(new StackLayout()
            {
                VerticalOptions = LayoutOptions.Center,
                Children = {
                        new ActivityIndicator() {
                            IsRunning = true
                        }
                    }
            });
    
            var worker = new BackgroundWorker();
    
            worker.DoWork += (object sender, DoWorkEventArgs e) => {
                var options = (SortedList<string, object>)e.Argument;
                try
                {
                    string args = "discard_view_option=1&orderBy=" + options["orderBy"] + "&order=" + options["order"] + "&page=" + options["page"] + "&count=" + options["count"] + "&real_values=1";
                    using (var req = new App.Network.PostRequest(URL, args))
                    {
                        req.DataReceived += (s, d) => {
                            e.Result = d.Data;
                        };
                        req.HTTPError += (s, d) => {
                            OnErrorLoading(d);
                        };
                        req.SendRequest();
                    }
                }
                catch (Exception ex)
                {
    #if DEBUG
                    Console.WriteLine(ex.Message);
                    throw;
    #endif
                }
    
            };
    
            worker.RunWorkerCompleted += (object sender, RunWorkerCompletedEventArgs e) => {
    
                if (e.Error != null)
                {
                    OnErrorLoading(new App.Network.HTTPErrorEventArgs
                    {
                        Code = 0,
                        Message = e.Error.Message
                    });
                }
                else
                {
                    if (!string.IsNullOrEmpty((string)e.Result))
                    {
    
                        List<Models.Task> result = JsonConvert.DeserializeObject<List<Models.Task>>((string)e.Result);
                        taskList.Children.Clear();
                        if (result.Count > 0)
                        {
                            foreach (Models.Task task in result)
                            {
                                var taskView = new Views.Task.ListItem();
                                taskView.BindingContext = task;
                                var taskTap = new TapGestureRecognizer();
                                taskTap.Tapped += (object s, EventArgs er) => {
    
                                    var page = new Pages.Object.View(task);
                                    page.Disappearing += (se, en) => {
                                        loadTasks();
                                    };
                                    OnOpenPage(page);
                                };
                                taskView.GestureRecognizers.Add(taskTap);
                                taskList.Children.Add(taskView);
                            }
                            var viewMore = new ContentView()
                            {
                                HeightRequest = 60,
                                VerticalOptions = LayoutOptions.Fill,
                                Content = new StackLayout()
                                {
                                    HorizontalOptions = LayoutOptions.Center,
                                    VerticalOptions = LayoutOptions.End,
                                    Children = {
                                            new Label() {
                                                Text = "View All Tasks >"
                                            }
                                        }
                                }
                            };
                            var moreTap = new TapGestureRecognizer();
                            moreTap.Tapped += (object s, EventArgs er) => {
                                var page = new Pages.Task.List();
                                OnOpenPage(page);
                            };
                            viewMore.GestureRecognizers.Add(moreTap);
                            taskList.Children.Add(viewMore);
                        }
                        else
                        {
                            taskList.Children.Add(new StackLayout()
                            {
                                VerticalOptions = LayoutOptions.Center,
                                Children = {
                                        new Label() {
                                            Text = "No Tasks Found"
                                        }
                                    }
                            });
                        }
    
                    }
                }
                OnFinishedLoading();
    
    
            };
    
            var listOptions = new SortedList<string, object>();
            listOptions.Add("orderBy", sort_by);
            listOptions.Add("order", sort_order);
            listOptions.Add("page", page);
            listOptions.Add("count", rowCount);
    
            worker.RunWorkerAsync(listOptions);
    
        }
    
    
    }
    
  • WammyWammy USMember ✭✭

    @JohnHardman the issue seems to come back if the page is reloaded.

    Ex: Load app - Dashboard page is loaded in a MasterDetail Page, use the MasterDetailPage to switch to another page, then switch back to Dashboard and the UI behaves the same way as before.

    Not sure what could be causing it to do this. Again iOS appears to function properly.

  • JohnHardmanJohnHardman GBUniversity ✭✭✭✭✭

    @Wammy - Apologies for delay in responding - just got back from vacation.

    My suspicion is still related to threading - are the handlers for DoWork and RunWorkerCompleted executed on the UI thread? I would assume for DoWork that the answer is no, and so invoking OnErrorLoading needs an explicit switch to the UI thread (Device.BeginInvokeOnMainThread). I don't know which thread RunWorkerCompleted executes on - if it is not the UI thread, you will need to add an explicit switch to the UI thread there too.

  • WammyWammy USMember ✭✭

    No problem @JohnHardman

    What I found is:

    "Yes, that is the main reason of being for a Backgroundworker. It has 3 events, only DoWork will be executed on a separate (ThreadPool) thread. Completed and ProgressChanged will be marshaled to the 'main' thread."

    To make sure, I have wrapped the calls to make changes to the UI in Device.BeginInvokeOnMainThread

    I really feel this may be an issue with the ScrollView not re-drawing when nested items are changed. And more so it really does appear to be in the Android implementation, and it is something new as of the last few versions, I had not encountered this before and the same code has been working before.

  • JohnHardmanJohnHardman GBUniversity ✭✭✭✭✭

    @Wammy - If you think it's a bug using Xamarin.Forms on Android, log it in Bugzilla with a small repro sample

    I've not had any issues with ScrollView not updating (miscalculating height yes, but not refusing to update)

  • NickKovalskyNickKovalsky USMember ✭✭

    Omg thanks guys saved my night. Was updating widths of childs in a non-ui thread and in iOS it was working perfectly. On android childs were reporting their width changed okay but my eyes had another image. Blamed scrollview for not updating showing me the real layout then google brought me here. Thanks again, solved. :)
    Device.BeginInvokeOnMainThread(() =>
    {
    // Update the UI
    child.WidthRequest = width;
    });

  • I'm seeing similar behaviour with the latest Xamarin.Forms but only on devices running Kit Kat and earlier and only when running a release build; using an older version of Xamarin.Forms, running on a newer device, or even a debug build restores the expected behaviour; Wammy did you ever file a bug report? If not I can try and create a minimal reproducible example to submit.

  • WammyWammy USMember ✭✭

    @PatrickDonnelly I have not submitted a bug report yet. I wanted to create a test project to make sure it is not my code doing it, but have not had the chance. If you want to submit a bug report let me know and I'll chime in with my notes.

Sign In or Register to comment.