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.
Related
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));
}
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.
I am using flowlistview for listing images. I need to hide the image if pageStatus value is OFF and show the image if pageStatus value is ON. I tried like below:
In model:
public string pageStatus { get; set; }
public bool pictureStatus
{
get
{
if (pageStatus == "OFF")
return false;
else
return true;
}
}
In XAML added IsVisible="{Binding pictureStatus}" for the image. The images are not showing in the UI but blank spaces are showing for OFF status pictures like below.
I need to remove that blank space also from the UI, how can I do that?
You can use IValueConverter easiliy:
XAML:
Define resource
xmlns:converter="clr-namespace:ConverterNamespace"
<flowlistview.Resources>
<ResourceDictionary>
<converter:VisibilityConverter x:Key="VisibilityConverter" />
</ResourceDictionary>
</flowlistview.Resources>
Binding
IsVisible="{Binding pageStatus, Converter={StaticResource VisibilityConverter}}"
Converter Class:
public class VisibilityConverter: IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((string)value == "ON" || (string)value != null)
return true;
else
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/data-binding/converters
If you want to change property value later and to notify listview, you need to use INotifyPropertyChanged as above post.
In your model:
public class ModelClass : INotifyPropertyChanged
{
string _pagestatus;
public string pageStatus
{
get
{
return _pagestatus;
}
set
{
_pagestatus = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string propName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
https://learn.microsoft.com/tr-tr/dotnet/api/system.componentmodel.inotifypropertychanged?view=netframework-4.7.2
You should filter the source data before binding to your list view's items source. When you find its value is ON, add it to your final items source. I modify your code in your PhotoGalleryViewModel like:
HttpClient client = new HttpClient();
var Response = await client.GetAsync("REST API");
if (Response.IsSuccessStatusCode)
{
string response = await Response.Content.ReadAsStringAsync();
PhotoAlbum photoAlbum = new PhotoAlbum();
List<PhotoList> dataList = new List<PhotoList>();
if (response != "")
{
photoAlbum = JsonConvert.DeserializeObject<PhotoAlbum>(response.ToString());
foreach (var photos in photoAlbum.photoList)
{
if (!Items.Contains(photos))
{
Items.Add(photos);
}
// Instead of using primitive data, filter it here
if (photos.pageStatus == "ON")
{
dataList.Add(photos);
}
}
AllItems = new ObservableCollection<PhotoList>(dataList);
await Task.Delay(TimeSpan.FromSeconds(1));
UserDialogs.Instance.HideLoading();
}
else
{
UserDialogs.Instance.HideLoading();
if (Utility.IsIOSDevice())
{
await Application.Current.MainPage.DisplayAlert("Alert", "Something went wrong, please try again later.", "Ok");
}
else
{
ShowAlert("Something went wrong, please try again later.");
}
}
}
else
{
UserDialogs.Instance.HideLoading();
if (Utility.IsIOSDevice())
{
await Application.Current.MainPage.DisplayAlert("Alert", "Something went wrong at the server, please try again later.", "Ok");
}
else
{
ShowAlert("Something went wrong at the server, please try again later.");
}
}
You have two options, either you remove the element completely from the ListView ItemsSource, or else, implement, in your viewmodel, the INotifyProperty changed, to change the visibility of each element. Here is a small sample.
using System;
using System.ComponentModel;
using Xamarin.Forms;
namespace YourNameSpace
{
public class YourViewModel : INotifyPropertyChanged
{
private string _pageStatus;
private bool _pictureStatus;
public event PropertyChangedEventHandler PropertyChanged;
public string PageStatus
{
set
{
if (_pageStatus != value)
{
_pageStatus = value;
if(_pageStatus.equals("OFF))
{
PictureStatus = false;
}
else
{
PictureStatus = true;
}
OnPropertyChanged("PageStatus");
}
}
get
{
return _pageStatus;
}
}
public bool PictureStatus
{
set
{
if (_pictureStatus != value)
{
_pictureStatus = value;
OnPropertyChanged("PictureStatus");
}
}
get
{
return _pictureStatus;
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Then, in XAML
IsVisible="{Binding PictureStatus}"
I have MainPageViewModel with Items (ObservableCollection). On this page I also have a button, that add new items to Items.
public class MainPageViewModel : Screen {
private DateTime StartActivity = DateTime.MinValue;
public ObservableCollection<ActivityViewModel> Items { get; set; }
public MainPageViewModel(INavigationService navigationService) {
this.Items = new ObservableCollection<ActivityViewModel>();
}
public void AddActivity(string activityName) {
if (this.Items.Count == 0) {
this.Items.Add(new ActivityViewModel() {
Activity = activityName,
Duration = 0
});
StartActivity = DateTime.Now;
}
else {
this.Items[this.Items.Count - 1].Duration = 10;
this.Items.Add(new ActivityViewModel() {
Activity = activityName,
Duration = 0
});
StartActivity = DateTime.Now;
}
}
}
Adding new items works perfect.
But data from Items not recovers when app activates after tombstoning. Try create StorageHandler for my ViewModel. Doesn't help. What I'm doing wrong?
public class MainPageViewModelStorage : StorageHandler<MainPageViewModel> {
public override void Configure() {
Property(x => x.Items)
.InAppSettings()
.RestoreAfterActivation();
}
}
Also try add [SurviveTombstone] for class and for property but Visual Studio don't know that attribute.
public class ActivityViewModel : PropertyChangedBase {
private string _activity;
public string Activity {
get {
return _activity;
}
set {
if (value != _activity) {
_activity = value;
NotifyOfPropertyChange(() => Activity);
}
}
}
private double _duration;
public double Duration {
get {
return _duration;
}
set {
if (value != _duration) {
_duration = value;
NotifyOfPropertyChange(() => Duration);
}
}
}
}
You should store not InAppSettings but InPhoneState.
Check with breakpoint if method Configure is called. If not - something wrong with your bootstrapper. Probably PhoneContainer.RegisterPhoneServices() is missing
Turn on catching first chance exception in Visual Studio (Ctrl+Alt+E, and put checkbox against CLR Exceptions). Probably your view model cannot be deserialized properly.
Noob question probably.
I am developing a mvm wp7 app where the map shows pushpins of salons. The database is retrieved from a link.
The problem i am struggling with is that the observable collection data is not being loaded from the App._ViewModel (where the json serializer parses the database and works fine). On debugging the app shows a plain map and thats all. On returning a string attribute from the database causes a break on that code. i tried messagebox as well to show the string, still crashes.
Heres the code:
mainviewmodel.cs
public class MainViewModel
{
public bool IsDataLoaded { get; private set; }
public ObservableCollection<SalonViewModel> SalonCollection { get; private set; }
public MainViewModel()
{
IsDataLoaded = false;
}
public ObservableCollection<SalonViewModel> LoadData()
{
SalonCollection = new ObservableCollection<SalonViewModel>();
var wednesday = new Uri("http://blehbleh.txt");
WebClient wc = new WebClient();
wc.OpenReadCompleted += new OpenReadCompletedEventHandler(wc_OpenReadCompleted);
wc.OpenReadAsync(wednesday);
return SalonCollection;
}
public void wc_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
try
{
DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(ObservableCollection<SalonViewModel>));
ObservableCollection<SalonViewModel> list = serializer.ReadObject(e.Result) as ObservableCollection<SalonViewModel>;
foreach (SalonViewModel b in list)
{
SalonCollection.Add(new SalonViewModel { sid=b.sid,sname=b.sname,sgeo_lat=b.sgeo_lat,sgeo_lon=b.sgeo_lon,
}
this.IsDataLoaded = true;
}
catch (Exception ex)
{
//throw ex;
MessageBox.Show(ex.Message);
}
}
The App.cs
public partial class App : Application
{
private static MainViewModel viewModel;
public static MainViewModel _viewModel
{
get
{
if (viewModel == null)
{
viewModel = new MainViewModel();
}
return viewModel;
}
}
void LoadData()
{
if (!_viewModel.IsDataLoaded)
{
_viewModel.LoadData();
}
}
etc
Heres the mappage.cs
private void salon_map_Loaded (object sender, RoutedEventArgs e)
{
foreach (SalonViewModel Salon in App._viewModel.LoadData)
{
MessageBox.Show(Salon.sname);
Pushpin p = new Pushpin();
p.Content = Salon.sname + System.Environment.NewLine + "Rate: ";
Layer.AddChild(p, new GeoCoordinate(Salon.sgeo_lon, Salon.sgeo_lat));
}
Map1.Children.Add(Layer);
}
In your MainViewModel LoadData function, OpenReadAsync() is an asynchronous function, and thus returning SalonCollection on the next line will return an empty ObservableCollection, since the callback function wc_OpenReadCompleted has not run yet.
Also, the reason the MessageBox.Show crashes is because you are attempting to call a UI function on a non-UI thread (solution to that here: Dispatcher.Invoke() on Windows Phone 7?)
Instead of returning the ObservableCollection and manually adding children to the map from that, try binding a MapItemsControl layer of the Map to the ObservableCollection of your view model. There's a decent example of doing that here: Binding Pushpins to Bing Maps in Windows Phone