Interesting (at least for me) bug I found there.
I am making an (prototype) app, it does some web requests and returns simple data.
There is ObservableCollection<DownloadbleEntity>
which is updated dynamicly (because DownloadbleEntity
contains the image which we get by other requests, to output list element with an image).
Here is layout part:
<MvvmCross.Binding.Droid.Views.MvxListView
android:id="@+id/searchlist"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
local:MvxBind="ItemsSource FoundItems; ItemClick OnItemClickCommand; OnScrollToBottom GetNewAsyncCommand"
local:MvxItemTemplate="@layout/listitem" />
And this is the ViewModel code to show an idea of how update is going:
private ObservableCollection<DownloadableEntity> _foundItems;
public ObservableCollection<DownloadableEntity> FoundItems
{
get { return _foundItems; }
set
{
if (_currentPage > 0)
{
_foundItems = new ObservableCollection<DownloadableEntity>(_foundItems.Concat(value));
}
else
{
_foundItems = value;
}
RaisePropertyChanged(() => FoundItems);
}
}
private async Task PrepareDataForOutput(SearchResult searchResult)
{
_currentListLoaded = false;
IMvxMainThreadDispatcher dispatcher = Mvx.Resolve<IMvxMainThreadDispatcher>();
List<Task<DownloadableEntity>> data = searchResult.Tracks.Items.ToList().Select(async (x) => await PrepareDataOutputAsync(x).ConfigureAwait(false)).ToList();
Android.Util.Log.Verbose("ACP", "PrepareDataForOutput");
try
{
var result = new ObservableCollection<DownloadableEntity>();
while (data.Count > 0)
{
var entityToAdd = await Task.Run(async () =>
{
Task<DownloadableEntity> taskComplete = await Task.WhenAny(data).ConfigureAwait(false);
data.Remove(taskComplete);
DownloadableEntity taskCompleteData = await taskComplete.ConfigureAwait(false);
await Task.Delay(500);
return taskCompleteData;
}).ConfigureAwait(false);
result.Add(entityToAdd);
// as it recommended by mvvmcross providers
dispatcher.RequestMainThreadAction(async () =>
await Task.Run(() =>
{
Android.Util.Log.Verbose("ACP", $"RequestMainThreadAction update {result.Last().Title}");
_toastService.ShowToastMessage($"Got {result.Last().Title}");
FoundItems = result;
}).ConfigureAwait(false)
);
}
await Task.WhenAll(data).ContinueWith((x) =>
{
Android.Util.Log.Verbose("ACP", "Output is Done");
_currentListLoaded = true;
});
}
catch (Exception e)
{
Android.Util.Log.Verbose("ACP", e.Message);
}
}
private async Task<DownloadableEntity> PrepareDataOutputAsync(PurpleItem x)
{
return new DownloadableEntity
{
Title = x.Title,
ArtistName = x.Artists.Select(y => y.Name).Aggregate((cur, next) => cur + ", " + next),
Image = await Task.Run(() => _remoteMusicDataService.DownloadCoverByUri(x.Albums.FirstOrDefault().CoverUri)).ConfigureAwait(false),
AlbumName = x.Albums.First().Title ?? "",
AlbumId = x.Albums.First().Id,
TrackId = x.Id
};
}
Well, the thing is - on devices, after data output starts it outputs one list element and at the same time outputs this:
Time Device Name Type PID Tag Message 12-09 13:45:22.012 Wileyfox Swift 2 X Info 20385 mvx android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6898) at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1048) at android.view.View.requestLayout(View.java:19785) at android.view.View.requestLayout(View.java:19785) at android.view.View.requestLayout(View.java:19785) at android.view.View.requestLayout(View.java:19785) at android.view.View.requestLayout(View.java:19785) at android.widget.AbsListView.requestLayout(AbsListView.java:1997) at android.widget.AdapterView$AdapterDataSetObserver.onChanged(AdapterView.java:840) at android.widget.AbsListView$AdapterDataSetObserver.onChanged(AbsListView.java:6380) at android.database.DataSetObservable.notifyChanged(DataSetObservable.java:37) at android.widget.BaseAdapter.notifyDataSetChanged(BaseAdapter.java:50)
But it continues to update my collection, so Android.Util.Log.Verbose("ACP", $"RequestMainThreadAction update {result.Last().Title}");
toggles and I can see it in device log window.
But it all continues to render on my device screen only after I do something - touch screen or touch my SearchView
input or rotate it.
It's kinda strange I wonder what causes it. Is it because of I do something wrong regarding my collection update?
I recorded the video of whats happening, so here it is (sorry for my english and accent :( )
UPD (regarding to last comment):
Is the collection really being update from the background thread if update is happening inside dispatcher.RequestMainThreadAction
?
UPD2
I added thread number detection, so, looks like the number is always the same
The code:
// as it recommended
dispatcher.RequestMainThreadAction(async () =>
await Task.Run(() =>
{
var poolId = TaskScheduler.Current.Id;
Android.Util.Log.Verbose("ACP THREAD INFO", $"RequestMainThreadAction update {result.Last().Title} THREAD NUMBER {poolId}");
_toastService.ShowToastMessage($"Got {result.Last().Title} by {result.Last().ArtistName}");
FoundItems = result;
}).ConfigureAwait(false)
);
Also in the View
I added:
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
_searchView = FindViewById<SearchView>(Resource.Id.search10);
ViewModel.OnSearchStartEvent += ViewModel_OnSearchStartEvent;
var poolId = TaskScheduler.Current.Id;
Android.Util.Log.Verbose("ACP THREAD INFO", $"VIEW CREATED FROM THREAD NUMBER {poolId}");
}
The output:
Also, tried this approach but no success:
Mvx.Resolve<IMvxAndroidCurrentTopActivity>().Activity.RunOnUiThread(async () =>
await Task.Run(() =>
{
var poolId = TaskScheduler.Current.Id;
Android.Util.Log.Verbose("ACP THREAD INFO", $"RequestMainThreadAction update {result.Last().Title} THREAD NUMBER {poolId}");
_toastService.ShowToastMessage($"Got {result.Last().Title} by {result.Last().ArtistName}");
FoundItems = result;
}).ConfigureAwait(false)
);
UPD3
I found one workaround (but still not solution) - if I use MvxObservableCollection
instead of just ObservableCollection
for FoundItems
- everything working as it suppose to!
If we look at this class (MvxObservableCollection.cs ) we will see that it has those functions which are triggering on updates, looks like it does the same thing there:
protected virtual void InvokeOnMainThread(Action action)
{
var dispatcher = MvxSingleton<IMvxMainThreadDispatcher>.Instance;
dispatcher?.RequestMainThreadAction(action);
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
InvokeOnMainThread(() => base.OnPropertyChanged(e));
}
But I don't get it why in my case it's not working as it suppose to, I mean with just regular ObservableCollection
?
Is notyfier for ObservableCollection
change creates another thread or what?
dispatcher.RequestMainThreadAction
is enough, or it isn't? – ScreakTask.Run
a lot of places. – BreauxTask.Run
wrapper, I got same picture :( – Screak