NilkOne
NilkOne

Reputation: 79

await MessagingCenter in Xamarin Forms

I tried to implement MVVM using MessagingCenter from my ViewModel. I obtained the following error because multiples threads receive the same Message "ClearStackLayout" and don't wait the callback's end of each other :

Index was outside the bounds of the array.

Here is my View code :

public partial class LibraryChoicePage : DefaultBackgroundPage {

        private Object thisLock = new Object();

        public LibraryChoicePage() {
            InitializeComponent();

            /* ClearStackLayout */
            MessagingCenter.Subscribe<LibraryChoiceViewModel>(this, "ClearStackLayout", (sender) => {
                lock (thisLock) {
                    this._choices.Children.Clear();
                }
            });

            /* AddToStackLayout */
            MessagingCenter.Subscribe<LibraryChoiceViewModel, View>(this, "AddToStackLayout", (sender, arg) => {
                lock (thisLock) {
                    this._choices.Children.Add(arg);
                }
            });

        }

    }

Upvotes: 1

Views: 6093

Answers (1)

SushiHangover
SushiHangover

Reputation: 74094

The number one thing is always call StackLayout.Children.Clear|Add on the UI thread. iOS does not like when removing UIView subviews off the main UI thread and will throw exceptions and can even cause native crashes

This is how I would serialized the messaging calls:

var semaphone = new SemaphoreSlim(1);
MessagingCenter.Subscribe<object>(this, "ClearStackLayout",  async (sender) =>
{
    await semaphone.WaitAsync();
    Device.BeginInvokeOnMainThread(() =>
    {
        _choices.Children.Clear();
    });
    semaphone.Release();
});

MessagingCenter.Subscribe<object, View>(this, "AddToStackLayout", async (sender, arg) =>
{
    await semaphone.WaitAsync();
    Device.BeginInvokeOnMainThread(() =>
    {
        _choices.Children.Add(arg);
    });
    semaphone.Release();
});

Note: try/finally should be wrapping the SemaphoreSlim.Release and a catch to execute any recoverer code needed from add/clear failures.

UIUnit Parallel Test Method:

Random random = new Random();
var tasks = new List<Task>();
for (int i = 0; i < 50; i++)
{
    if (random.NextDouble() > .1)
        tasks.Add(Task.Factory.StartNew(() => { AddLayout(); }));
    else
        tasks.Add(Task.Factory.StartNew(() => { ClearLayout(); }));
}
var completed = Task.Factory.ContinueWhenAll(tasks.ToArray(), (messagecenterTasks) => { 
    foreach (var task in messagecenterTasks)
    {
        if (task.Status == TaskStatus.Faulted)
        {
            D.WriteLine("Faulted:");
            D.WriteLine($"  {task.Exception.Message}");
        }
    }
}).Wait(1000);
if (!completed)
    D.WriteLine("Some tasks did not complete in time allocated");

Note: AddLayout/ClearLayout are method wrappers for the MessageCenter.Send of AddToStackLayout and ClearStackLayout.

Upvotes: 8

Related Questions