How to prevent UI changes on the wrong thread?

Rainer 166 Reputation points
2021-07-04T01:39:25.85+00:00

I have my Xamarin Form app separated into separate projects, one for the main Forms App and one for some business logic classes including my View Model classes (plus one for iOS and one for Android). I'm running into a problem where a change event in my view model cause the UI handles to update, but that's not happening on the main UI thread, which causes an exception.

The important objects in this scenario are:

  1. State, which holds various values including a list of Offers. It emits change events when these values change.
  2. DataManager, which handles all service calls and updates State with the results of those calls.
  3. ViewModel, which exposes appropriate properties the Page can bind to, including a reference to State.

Here's the basic timeline:

  1. The page loads. Inside the page I have a RefreshView and inside that is a CarouselView with ItemsSource="{Binding State.Offers}".
  2. In Page.OnAppearing I set ViewModel.IsRefreshing=true
  3. This initiates an event which causes the view model to call my Refresh command
  4. In the Refresh command I call a function in my library like this _ = await DataManager.GetActiveOffersAsync()
  5. Inside that library function it does multiple other async calls, which are configured with ConfigureAwait(false) like await ServiceConnector.GetActiveOffersAsync().ConfigureAwait(false); eventually it updates State.Offers with the new data
  6. State.Offers emits a change event, and since my CarouselView's ItemsSource is bound to that it then tries to update the UI
  7. This UI update fails with an exception: UIKit.UIKitThreadAccessException: UIKit Consistency error: you are calling a UIKit method that can only be invoked from the UI thread.

My understanding is that the reason this happens is because of my use of ConfigureAwait(false), which allows those async calls to continue on a different thread. This is recommended for API libraries, which is what DataManager is. I know there is protection for this with Device.BeginInvokeOnMainThread, but I don't really have anywhere to use that. My State object is in a non-Xamarin project, so it doesn't know about Device. I also feel like a library or business object like this shouldn't need to care about its downstream usage to this degree. It seems to me that Xamarin Forms shouldn't be listening to change events in bound properties in a way that it can't handle and that inside Forms would be the appropriate place to ensure the updates happen on the UI thread.

Does anyone have any suggestions of an elegant way to handle this situation?

Xamarin
Xamarin
A Microsoft open-source app platform for building Android and iOS apps with .NET and C#.
5,354 questions
{count} votes

1 answer

Sort by: Most helpful
  1. Kyle Wang 5,531 Reputation points Microsoft Vendor
    2021-07-07T07:55:13.197+00:00

    Hi Rainer-M,

    Welcome to our Microsoft Q&A platform!

    In my test, the event "PropertyChanged" in your project has never been triggered. So I directly modified ObsOffers in "DoRefresh". And everything works fine, the UI updates in succeed.

    It seems like you want to update the collectionview with new items. Here is my way to update the CollectionView and it works fine in both Android and iOS.
    xaml:

    <StackLayout Padding="0,40,0,0">  
        <Button Text="Update" Command="{Binding UpdateCommand}"/>  
        <ScrollView VerticalScrollBarVisibility="Never">  
            <CollectionView   
            BackgroundColor="Red"  
            ItemsSource="{Binding ObsOffers}">  
                <CollectionView.ItemTemplate>  
                    <DataTemplate>  
                        <Label Text="{Binding ID}" />  
                    </DataTemplate>  
                </CollectionView.ItemTemplate>  
            </CollectionView>  
        </ScrollView>  
    </StackLayout>  
    

    xaml.cs:

    public partial class MainPage : ContentPage  
    {  
        public MainPage()  
        {  
            InitializeComponent();  
            this.BindingContext = new TestViewModel();  
        }  
    }  
    

    And define the viewmodel in "ClassLibrary" project.

    public class TestViewModel : INotifyPropertyChanged  
    {  
        public ICommand UpdateCommand { get; }  
        public string Text { get; set; }  
      
        string state;  
        public string Stata  
        {  
            get => state;  
            set  
            {  
                state = value;  
                OnPropertyChanged("State");  
            }  
        }  
        public ObservableCollection<Offer> ObsOffers { get; } = new ObservableCollection<Offer>();  
      
        public TestViewModel()  
        {  
            for (int i = 0; i < 10; i++)  
            {  
                ObsOffers.Add(new Offer { ID = "ID" + i });  
            }  
      
            UpdateCommand = new Command(Update);  
        }  
      
        void Update()  
        {  
            Stata = "stata changed";  
        }  
      
        public event PropertyChangedEventHandler PropertyChanged;  
        protected void OnPropertyChanged(string propertyName)  
        {  
            var handler = PropertyChanged;  
            if (handler != null)  
                handler(this, new PropertyChangedEventArgs(propertyName));  
      
            if(propertyName == "State")  
            {  
                Console.WriteLine("stata changed");  
                ObsOffers.Clear();  
                for (int i = 0; i < 10; i++)  
                {  
                    ObsOffers.Add(new Offer { ID = "NewID" + i });  
                }  
            }  
        }  
    }  
    

    Regards,
    Kyle


    If the response is helpful, please click "Accept Answer" and upvote it.

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.