ItemsSource doesn't show/bind when ObservableCollection is filled before loading the page - xamarin

I have a carouselview, in that view I have an ObservableCollection binded as an itemssource. I am able to bind the collection and it would show when I execute the viewmodel's command in the OnAppearing event.
Code that works:
Second Page
public partial class SecondPage : ContentPage
{
public Coll(bool hard, string subject)
{
InitializeComponent();
var vm = (DataSelectionViewModel)BindingContext;
vm.Hard = hard;
vm.Subject = subject;
/* had to set "hard" and "subject" here again, otherwise data won't load */
}
protected override async void OnAppearing()
{
var vm = (DataSelectionViewModel)BindingContext;
base.OnAppearing();
await vm.LoadData.ExecuteAsync().ConfigureAwait(false);
}
}
The viewmodel for second page
public class DataSelectionViewModel : BaseViewModel
{
private string subject;
public string Subject { get => subject; set => SetProperty(ref subject, value); }
private bool hard;
public bool Hard { get => hard; set => SetProperty(ref hard, value); }
public ObservableCollection<Items> FilteredData { get; set; }
public UserSelectionViewModel()
{
_dataStore = DependencyService.Get<IDataStore>();
LoadData= new AsyncAwaitBestPractices.MVVM.AsyncCommand(FilterData);
FilteredData = new ObservableCollection<Items>();
}
public async Task FilterData()
{
FilteredData.Clear();
var filtereddata = await _dataStore.SearchData(Hard, Subject).ConfigureAwait(false);
foreach (var data in filtereddata)
{
FilteredData.Add(data);
}
}
}
First Page where second page gets Hard and Subject values
private async void ButtonClick(object sender, EventArgs e)
{
var vm = (BaseViewModel)BindingContext;
vm.Hard = HardButtonSelected == Hard;
vm.Subject = vm.Subject.ToLower();
await Navigation.PushAsync(new SecondPage(vm.Hard, vm.Subject));
}
So I want to change my code so that if I press the button on the first page, data instantly starts to filter and add to the ObservableCollection and when it's finished, then navigate to the second page. However if I try to load it to the BaseViewModel and then get the data from the second viewmodel it won't show the data.
Code that doesn't work:
Second Page
public partial class SecondPage : ContentPage
{
public SecondPage()
{
InitializeComponent();
}
}
The viewmodel for second page
public class DataSelectionViewModel : BaseViewModel
{
public ObservableCollection<Items> FilteredData { get; set; }
public UserSelectionViewModel()
{
FilteredData = new ObservableCollection<Items>();
}
}
BaseViewModel
public class BaseViewModel : INotifyPropertyChanged
{
private string subject;
public string Subject { get => subject; set => SetProperty(ref subject, value); }
private bool hard;
public bool Hard { get => hard; set => SetProperty(ref hard, value); }
public ObservableCollection<Items> FilteredData { get; set; }
/* BaseViewModel has implementation of SetProperty */
}
First Page where second page gets Hard and Subject values
private async void ButtonClick(object sender, EventArgs e)
{
var vm = (BaseViewModel)BindingContext;
vm.Hard = HardButtonSelected == Hard;
vm.Subject = vm.Subject.ToLower();
}
First Page viewmodel
public class FirstPageViewModel : BaseViewModel
{
public IAsyncCommand MehetButtonClickedCommand { get; }
readonly IPageService pageService;
readonly IFeladatokStore _feladatokStore;
public FeladatValasztoViewModel()
{
_dataStore = DependencyService.Get<IDataStore>();
ButtonClickedCommand = new AsyncCommand(ButtonClicked);
pageService = DependencyService.Get<IPageService>();
}
private async Task ButtonClicked()
{
await FilterData();
await pageService.PushAsync(new SecondPage());
}
private async Task FilterData()
{
FilteredData.Clear();
var datas = await _dataStore.SearchData(Subject, Hard).ConfigureAwait(false);
foreach (var data in datas)
{
FilteredData.Add(data);
}
}
So basically this gives a null exception error. I also tried giving the ObservableCollection as an argument for SecondPage(ObservableCollection x) and that did work, but because I had to make another ObservableCollection for it and copy from one to another it stopped being async and froze for a couple of seconds. So my question is how can I make this async?

To avoid delay, build the new collection in a private variable. Then set the property to that variable:
// Constructor with parameter
public SomeClass(IList<Items> data)
{
SetFilteredDataCopy(data);
}
public ObservableCollection<Items> FilteredData { get; set; }
private void SetFilteredDataCopy(IList<Items> src)
{
var copy = new ObservableCollection<Items>();
foreach (var item in src)
copy.Add(item);
FilteredData = copy;
//MAYBE OnPropertyChanged(nameof(FilteredData));
}

Related

DataBinding issue in Xamarin Forms

I have a new Xamarin Forms 5 app and I'm having trouble with data binding.
First, I display a message that tells the user how many items are in his list. Initially, this is 0. It's displayed by DisplayMessage property of the view model.
Then, the Init() method gets called and once the API call is finished, there are some items in MyList. I put break points to make sure that the API call works and I end up with some data in MyList property.
Because I change the value of message in my Init() method, I was expecting the message to change and display the number of items in the list but it's not changing even though I have some items in MyList.
I created a new ViewModel that looks like this:
public class MyViewModel : BaseViewModel
{
public List<MyItem> MyList { get; set; } = new List<MyItem>();
string message = "You have no items in your list... ";
public string DisplayMessage
{
get => message;
set
{
if(message == value)
return;
message = value;
OnPropertyChanged();
}
}
public async void Init()
{
var data = await _myService.GetData();
if(data.Count > 0)
message = $"You have {data.Count} items in your list!";
MyList = data;
}
}
My MainPage code behind looks like this:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MainPage : ContentPage
{
MyViewModel _vm;
MainPage()
{
InitializeComponent();
_vm = new MyViewModel();
this.BindingContext = _vm;
}
protected override void OnAppearing()
{
base.OnAppearing();
_vm.Init();
}
}
I didn't change anyting in the base view model, except I added my service and it looks like this:
public class BaseViewModel : INotifyPropertyChanged
{
public IMyApiService MyApi => DependencyService.Get<IMyApiService>();
bool isBusy = false;
public bool IsBusy
{
get { return isBusy; }
set { SetProperty(ref isBusy, value); }
}
string title = string.Empty;
public string Title
{
get { return title; }
set { SetProperty(ref title, value); }
}
protected bool SetProperty<T>(ref T backingStore, T value,
[CallerMemberName] string propertyName = "",
Action onChanged = null)
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
I'd appreciatae someone telling me where my mistake is. Thanks.
Without seeing the Xaml, I can't 100% answer, but here are a couple of things I see:
You are setting the "message" through the field, not the property. Since you are setting the field directly the OnPropertyChanged event isn't firing so the UI isn't getting notified that the value has changed.
I am guessing you are binding "MyList" to some sort of CollectionView or something? If it's a readonly view, using a List is ok as the collection is never updated. However, if you plan on adding or removing items at runtime, it needs to be an "ObservableCollection" for the same reason as above, the UI isn't notified of new items in a List, but an ObservableCollection will notify the UI of changes to it, so it can update.
Is what Jason mentions above in his comment. The MyList property should be setup like the other properties with the OnPropertyChanged.

Xamarin Picker not updating after api call

I have a xamarin picker that should display a list of countries after getting them from the api (from inside viewmodel), but when I set the itemsource to a List variable the picker doesn't update.
public Departures(DeparturesViewModel mod)
{
InitializeComponent();
model = mod;
GetCountryData();
}
private async void GetCountryData()
{
var res= await model.SetCountries();// load api data
CountryPikcer.IsEnabled = true;
CountryPikcer.ItemDisplayBinding = new Binding("Name");//Set the name property as the display property
CountryPikcer.ItemsSource = model.FilterCountries("");//get loaded List<Country>
}
ViewModel:
private List<Country> countries;
public int CountryId
{
get { return countryId; }
set { SetProperty(ref countryId, value); }
}
public DeparturesViewModel()
{
api = new ApiCaller();
countries = new List<Country>();
}
public async Task<bool> SetCountries()
{
countries = await api.GetAll<List<Country>>("Countries");
return true;
}
public List<Country> FilterCountries (string text)
{
if (text == "")
return countries;
List<Country> filtered = countries.Where(x => x.Name.Contains(text)).ToList();
return filtered;
}
Inside the debugger the ItemsSource property is getting populated but the picker is not
In my opinion your problem is in viewmodel. You are using async call that means that all your controls got rendered before data from async call is available. In this case your viewmodel should implement INotifyPropertyChanged. Then for example:
public List<Country> Countries
{
{
set { countries = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Countries))); }
get { return countries; }
}
}
To ensure that controls data is refreshing properly.

Chain CollectionChanged Events

I'm trying to see how it would be possible to chain together x number of ObservableCollections.CollectionChanged event, exposed as a N level depth object tree to a single parent level CollectionChanged event that consumers can listen to? Essentially I want to funnel or bubble all child CollectionChanged events up to the top most parent. A number of solution I've noticed that tackle similar issues make an assumption of a fixed number of levels, say 2 deep. I idea is to support any level of depth.
Originally I had hoped I could just pass the instance of the FieldInfos to the child constructors and attach directly to the handler. However i get an error stating the "Event 'CollectionChanged' can only appear on the left hand side of+= or -=.
Thanks,
public class FieldInfos
{
public event NotifyCollectionChangedEventHandler CollectionChanged;
private ObservableCollection<Field> _fields;
public ObservableCollection<Field> Fields => _fields ?? (_fields = new ObservableCollection<Field>());
}
public class Field
{
public string Name;
private ObservableCollection<FieldInstance> _instances;
public ObservableCollection<FieldInstance> Instances => _instances ?? (_instances = new ObservableCollection<FieldInstance>());
}
public class FieldInstance
{
public string Id { get; set; }
}
The simplest approach is subclass the original ObservableCollection<T>.
You'd need at least one interface to avoid covariance problems. You can also have your own classes to implement the INotifyDescendantsChanged interface.
public interface INotifyDescendantsChanged
{
event NotifyCollectionChangedEventHandler DescendantsChanged;
}
public class ObservableBubbleCollection<T> : ObservableCollection<T>, INotifyDescendantsChanged
{
public event NotifyCollectionChangedEventHandler DescendantsChanged;
protected virtual void OnDescendantsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChangedEventHandler handler = DescendantsChanged;
if (handler != null)
handler(sender, e);
}
private readonly Func<T, INotifyDescendantsChanged> childSelector;
public ObservableBubbleCollection() { }
public ObservableBubbleCollection(Func<T, INotifyDescendantsChanged> childSelector)
{
this.childSelector = childSelector;
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
OnDescendantsChanged(this, e);
if (childSelector == null)
return;
if (e.NewItems != null)
foreach (var item in e.NewItems.Cast<T>())
childSelector(item).DescendantsChanged += OnDescendantsChanged;
if (e.OldItems != null)
foreach (var item in e.OldItems.Cast<T>())
childSelector(item).DescendantsChanged -= OnDescendantsChanged;
}
}
To use it, replace instances of ObservableCollection and pass a selector to the collection.
public class FieldInfos
{
private ObservableBubbleCollection<Field> _fields;
public ObservableBubbleCollection<Field> Fields => _fields ?? (_fields = new ObservableBubbleCollection<Field>(fi => fi.Instances));
}
public class Field
{
public string Name;
private ObservableBubbleCollection<FieldInstance> _instances;
public ObservableBubbleCollection<FieldInstance> Instances => _instances ?? (_instances = new ObservableBubbleCollection<FieldInstance>());
}
public class FieldInstance
{
public string Id { get; set; }
}
static class Program
{
static void Main(string[] args)
{
var fi = new FieldInfos();
fi.Fields.DescendantsChanged += (sender, e) =>
{
Console.WriteLine("Change from {0}", sender.GetType());
};
var field = new Field();
fi.Fields.Add(field);
field.Instances.Add(new FieldInstance());
Console.ReadLine();
}
}

Xamarin Forms - Update UI element from itself doesn't work

I'm currently working on Map UI update.
I have a public List<string> PolylineAddressPoints which has a setter where a method is called. The method, which is called, updates public List<GeoPosition> PolylineCoordinates.
When the public List<GeoPosition> PolylineCoordinates has been update, then the renderer is updating the UI of my map.
However, it only works if I edit the list from the MainPage.xaml.cs.
I search during a long moment on the web why my map want not update by itself without find any solution. Does the elements needs to be updated from the UI thread?
My main goal is just to update it directly from the list..
My customMap:
public class CustomMap : Map
{
public static readonly BindableProperty PolylineAddressPointsProperty =
BindableProperty.Create(nameof(PolylineAddressPoints), typeof(List<string>), typeof(CustomMap), new List<string>(), BindingMode.TwoWay);
public List<string> PolylineAddressPoints
{
get { return (List<string>)GetValue(PolylineAddressPointsProperty); }
set
{
SetValue(PolylineAddressPointsProperty, value);
generatePolylineCoordinates(value);
}
}
public static readonly BindableProperty PolylineCoordinatesProperty =
BindableProperty.Create(nameof(PolylineCoordinates), typeof(List<GeoPosition>), typeof(CustomMap), new List<GeoPosition>(), BindingMode.TwoWay);
public List<GeoPosition> PolylineCoordinates
{
get { return (List<GeoPosition>)GetValue(PolylineCoordinatesProperty); }
set { SetValue(PolylineCoordinatesProperty, value); }
}
private async void generatePolylineCoordinates(List<string> value)
{
this.PolylineCoordinates = await GeneratePolylineCoordinates(value);
MessagingCenter.Send<App>((App)Application.Current, "PolylineUpdated");
}
}
I tried using the MessagingCenter but it doesn't works.. It look like the thread is cancel, I said that because I don't see the debug as well.. I don't understand what is happening..
There is the MainPage.xaml.cs:
public partial class MapPolylinePage : ContentPage
{
public List<string> AddressPointList { get; set; }
public MapPolylinePage()
{
base.BindingContext = this;
MessagingCenter.Subscribe<App>((App)Application.Current, "PolylineUpdated", (sender) =>
{
Debug.WriteLine("--DEBUG ADDRESS--");
foreach (string item in MapTest.PolylineAddressPoints)
{
Debug.WriteLine("Adress => [{0}]", item);
}
Debug.WriteLine("--DEBUG ADDRESS--");
Debug.WriteLine("--DEBUG POSITION--");
foreach (GeoPosition item in MapTest.PolylineCoordinates)
{
Debug.WriteLine("GeoPosition => [{0}/{1}]", item.Latitude, item.Longitude);
}
Debug.WriteLine("--DEBUG POSITION--");
});
AddressPointList = new List<string>()
{
"Le Mans, 72100, France",
"Arnages, 72230, France",
"Allonnes, 72700, France"
};
InitializeComponent();
}
}
and the MainPage.xaml:
<ContentPage.Content>
<local:CustomMap x:Name="MapTest" PolylineAutoUpdate="True" PolylineAddressPoints="{Binding AddressPointList}" PolylineColor="Aqua" PolylineWidth="5"
VerticalOptions="Fill" HorizontalOptions="Fill"/>
</ContentPage.Content>
Thank in advance !

NavigationService throws NullReferenceException

Using MVVM Light, I'm trying to develop a rather simple WP7 application. I've run into a problem using the navigation service. I can navigate to a page, but after pressing the back button I can't navigate to the same page again. NavigationService throws a NullReferenceException.
I have implemented my navigation using Messaging from the GalaSoft.MvvmLight.Messaging namespace. All my views inherits from a customized PhoneApplicationPage base class that registrers a listener on "NavigationRequest":
public class PhoneApplicationPage : Microsoft.Phone.Controls.PhoneApplicationPage
{
public PhoneApplicationPage() : base()
{
Messenger.Default.Register<Uri>(this, "NavigationRequest", (uri) => NavigationService.Navigate(uri));
}
}
From my view models I post Uri's to this listener:
SendNavigationRequestMessage(new Uri("/View/AppSettingsView.xaml", UriKind.Relative));
Like i said, this works except when navigating after pressing the Back button.
Why is this and how can I solve it?
Is there a better way to implement navigation using MVVM Light?
I'm using MVVM Light as well. I have a class called PageConductor, which is based on what John Papa (Silverlight MVP) from Microsoft uses. Here's the PageConductor Service I use
public class PageConductor : IPageConductor
{
protected Frame RootFrame { get; set; }
public PageConductor()
{
Messenger.Default.Register<Messages.FrameMessage>(this, OnReceiveFrameMessage);
}
public void DisplayError(string origin, Exception e, string details)
{
string description = string.Format("Error occured in {0}. {1} {2}", origin, details, e.Message);
var error = new Model.Error() { Description = description, Title = "Error Occurred" };
Messenger.Default.Send(new Messages.ErrorMessage() { Error = error });
}
public void DisplayError(string origin, Exception e)
{
DisplayError(origin, e, string.Empty);
}
private void OnReceiveFrameMessage(Messages.FrameMessage msg)
{
RootFrame = msg.RootFrame;
}
private void Go(string path, string sender)
{
RootFrame.Navigate(new Uri(path, UriKind.Relative));
}
public void GoBack()
{
RootFrame.GoBack();
}
}
In my MainPage.xaml.cs constructor, I have this, which creates an instance of my ContentFrame in my PageConductor service.:
Messenger.Default.Send(new Messages.FrameMessage() { RootFrame = ContentFrame });
I then use dependency injection to instantiate an instance of my PageConductor Service into my MainPage ViewModel. Here is my MainViewModel class:
protected Services.IPageConductor PageConductor { get; set; }
public RelayCommand<string> NavigateCommand { get; set; }
public MainViewModel(Services.IPageConductor pageConductor)
{
PageConductor = pageConductor;
RegisterCommands();
}
private void RegisterCommands()
{
NavigateCommand = new RelayCommand<string>(
(source) => OnNavigate(source));
}
private void OnNavigate(string sender)
{
PageConductor.GoToView(sender, "main");
}
Notice the instance of my PageConductorService as a parameter in my MainViewModel constructor method. I pass this in via my ViewModelLocator:
private readonly TSMVVM.Services.ServiceProviderBase _sp;
public ViewModelLocator()
{
_sp = Services.ServiceProviderBase.Instance;
CreateMain(_sp);
}
#region MainPageViewModel
public static MainViewModel MainStatic
{
get
{
Services.ServiceProviderBase SP = Services.ServiceProviderBase.Instance;
if (_main == null)
{
CreateMain(SP);
}
return _main;
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance",
"CA1822:MarkMembersAsStatic",
Justification = "This non-static member is needed for data binding purposes.")]
public MainViewModel Main
{
get
{
return MainStatic;
}
}
public static void ClearMain()
{
_main.Cleanup();
_main = null;
}
public static void CreateMain(Services.ServiceProviderBase SP)
{
if (_main == null)
{
_main = new MainViewModel(SP.PageConductor);
}
}
#endregion
For further reference, my Messages.FrameMessage class is simply:
internal class FrameMessage
{
public Frame RootFrame { get; set; }
}
I've had no issues with forward/back buttons.

Resources