Associating ReactiveList <T> with ListView in Xamarin Formats
I feel like doing it right, but I'm not sure. I load objects in a ReactiveList in my ViewModel asynchronously. Through Xamarin Forms, I linked the List to the ItemSource ListView property in Xamarin Forms. I also have a search box that will clear the ReactiveList and add new elements to the ReactiveList.
The first time I open the view, no interaction causes the list to load, and the button associated with the ReactiveCommand to load more items in the ReactiveList is disabled.
The second time I open the View, the ListView immediately displays the items in the new ViewModel, but again the interaction does not work. However, changing the search box does clear items from the ListView, but items are not added.
This behavior applies to iOS. I am ReactiveUI 6.3.1 and Xamarin Forms 1.3.2.6316.
Here is a simplified task:
public class MyViewModel : ReactiveObject, IRoutableViewModel { public MyViewModel(IScreen hostScreen = null) { HostScreen = hostScreen ?? Locator.Current.GetService<IScreen>(); List = new ReactiveList<ItemViewModel>() { ChangeTrackingEnabled = true }; //This never gets shown List.Add(new ItemViewModel() { Name = "TEST" }); //Tried doing this on the main thread: same behavior LoadItemsCommand = ReactiveCommand.CreateAsyncTask(_ => GetItems()), RxApp.TaskpoolScheduler); LoadItemsCommand .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(results => { //debugger breaks here in EVERY case and successfully changes List but doesn't necessarily affect the view try { List.AddRange(results.Select(e => new ItemViewModel() { Name = e.Name })); } catch (Exception ex) { //breakpoint does not break here. Debug.WriteLine(ex.Message); throw; } }); //No exceptions here either LoadItemsCommand.ThrownExceptions .Select(ex => new UserError("Error", "Please check your Internet connection")) .Subscribe(Observer.Create<UserError>(x => UserError.Throw(x))); this.WhenAnyValue(e => e.SearchText).Subscribe(e => ResetPage()); } private Task<IEnumerable<Item>> GetItems() { //asynchronously get items return ...; } private int ResetPage() { List.Clear(); return 0; } [DataMember] public ReactiveList<ItemViewModel> List { get; private set; } private string _searchText; [DataMember] public string SearchText { get { return _searchText; } set { this.RaiseAndSetIfChanged(ref _searchText, value); } } public ReactiveCommand<IEnumerable<Item>> LoadItems { get; protected set; } public class ItemViewModel : ReactiveObject { public string Name { get; set; } } public string UrlPathSegment { get { return "Page"; } } public IScreen HostScreen { get; protected set; } } My xaml:
<StackLayout VerticalOptions="FillAndExpand" Orientation="Vertical"> <Entry x:Name="_searchEntry" Placeholder="Search"></Entry> <ListView x:Name="_myListView" RowHeight="80"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <StackLayout Orientation="Vertical" > <Label Text="{Binding Name}"></Label> <Label Text="{Binding Address}"></Label> <Label Text="{Binding PhoneNumber}"></Label> </StackLayout> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> <Button x:Name="_loadMoreButton" Text="Load More" TextColor="White" BackgroundColor="#77D065"></Button> </StackLayout> </XamForms:ReactiveContentPage> My code is:
using System; using System.Reactive.Concurrency; using System.Threading.Tasks; using ReactiveUI; using ReactiveUI.XamForms; namespace views { public partial class MyView : ReactiveContentPage<MyViewModel> { private IDisposable _disconnectHandler; public NearbyPlacesView() { InitializeComponent(); this.Bind(this.ViewModel, model => model.SearchText, view => view._searchEntry.Text); this.OneWayBind(this.ViewModel, model => model.List, view => view._myListView.ItemsSource); this.BindCommand(this.ViewModel, model => model.LoadItemsCommand, view => view._loadMoreButton); } protected override void OnAppearing() { base.OnAppearing(); //Is this the proper way to do this? seems _disconnectHandler = UserError.RegisterHandler(async error => { RxApp.MainThreadScheduler.ScheduleAsync(async (scheduler, token) => { await DisplayAlert("Error", error.ErrorMessage, "OK"); }); return RecoveryOptionResult.CancelOperation; }); //Load the items when the view appears. This doesn't feel right though. ViewModel.LoadItemsCommand.Execute(null); } protected override void OnDisappearing() { base.OnDisappearing(); _disconnectHandler.Dispose(); _disconnectHandler = null; } } } This seems to be a bug with Xamarin Forms or ReactiveUI. The problem is documented here: https://github.com/reactiveui/ReactiveUI/issues/806
I changed the type of public ReactiveList<ItemViewModel> List to public ObservableCollection<ItemViewModel> List , which fixed the problem.