I'm currently developing a Windows Phone 8 app that requires the use of a web API. When I decided to move towards the MVVM model I moved the necessary code to download data from the web API over to a separate class. After implementing all necessary functionality, I realized that the thread wouldn't wait for the data to finish downloading (or at least wouldn't wait for the necessary code to run following that), the thread would go through the ViewModel constructor and return a blank list to bind to my LongListSelector control. When debugging I realized that the thread would actually go through the DownloadCompleted method within my ViewModel at some point, but it was always after the ItemsSource for my LongListSelector was already set to a blank List. In the end I did get a properly populated list of data, it's just that the LongListSelector would have already been bound to the empty List instead. Is there anyway I can change what I'm doing so that my LongListSelector actually binds to the real data I'm getting from the web instead of binding to an empty List before it's properly populated with data? Whether it's somehow waiting for all the necessary code to run before the thread moves on, or updating the View when my List gets properly populated with data, I'm willing to accept any sorts of suggestions as long as they get my code to finally work.
Thanks in advance!
In MainPage.xaml.cs:
public void Retrieve()
{
MySets.ItemsSource = new MySetsViewModel(CurrentLogin).BindingList;
}
MySetsView Model is defined as the following:
public class MySetsViewModel
{
User CurrentLogin;
List<Sets> SetsList;
public List<Sets> BindingList { get; set; }
public MySetsViewModel(User CurrentLogin)
{
this.CurrentLogin = CurrentLogin;
Retrieve(CurrentLogin);
}
public async void Retrieve(User CurrentLogin)
{
if (IsolatedStorageSettings.ApplicationSettings.Contains("AccessToken"))
{
CurrentLogin.AccessToken = IsolatedStorageSettings.ApplicationSettings["AccessToken"].ToString();
}
if (IsolatedStorageSettings.ApplicationSettings.Contains("UserID"))
{
CurrentLogin.UserId = IsolatedStorageSettings.ApplicationSettings["UserID"].ToString();
}
try
{
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + CurrentLogin.AccessToken);
client.DefaultRequestHeaders.Add("Host", "api.quizlet.com");
var result = await client.GetAsync(new Uri("https://api.quizlet.com/2.0/users/" + CurrentLogin.UserId + "/sets"), HttpCompletionOption.ResponseContentRead);
string jsonstring = await result.Content.ReadAsStringAsync();
DownloadCompleted(jsonstring);
}
catch
{
}
}
void DownloadCompleted(string response)
{
try
{
//Deserialize JSON into a List called Sets
this.BindingList = Sets;
}
catch
{
}
}
}
All you have to do is implement INotifyPropertyChanged on your view model. Raise the PropertyChanged event inside the setter for "BindingList", and the view will update itself.
public class MySetsViewModel : INotifyPropertyChanged
{
public List<Sets> BindingList
{
get { return _bindingList; }
set
{
_bindingList = value;
RaisePropertyChanged("BindingList");
}
}
private List<Sets> _bindingList;
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Related
I recreated a new app maui and to make the navigation I redid what worked on another app that I made (on the other app it still works). As soon as I navigate on a new page everything goes well but it's as soon as I want to come back on the page where I was or I have a bug.
NavigationService.cs
public async Task NavigateAsync(NavigationPages page)
{
try
{
await Shell.Current.GoToAsync(page.ToString(), true);
}
catch (Exception ex)
{
}
}
ViewModel.cs
public ICommand GoBacKCommand { get; }
public ParamViewModel(INavigationService navigationService)
{
GoBacKCommand = new Command(GoBacKExecute);
this.navigationService = navigationService;
}
private void GoBacKExecute()
{
navigationService.NavigateAsync(BusinessModels.NavigationPages.MainPage);
}
MauiProgram.cs
Routing.RegisterRoute(nameof(MainPage), typeof(MainPage));
services.AddTransient<MainPage>();
services.AddTransient<MainViewModel>();
and the Exception is : Relative routing to shell elements is currently not supported. Try prefixing your uri with ///: ///MainPage
but if I do what it says (///MainPage) the navigation does not work anymore
How can I solve this problem?
If you want go back to a page (like PopUp) you need to need use GotoAsync as below
ViewModel.cs
public ICommand Back_Command { get; set; }
public ViewModel()
{
Back_Command = new Command(Back);
}
private async void Back()
{
await Shell.Current.GoToAsync("..");
}
Page.xaml
<Shell.BackButtonBehavior>
<BackButtonBehavior
Command="{Binding Back_Command}"/>
</Shell.BackButtonBehavior>
Further explanation has been stated in this documentation https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/shell/navigation
I am very new to c# so this could be very easy. I am busy creating a xamarin forms app and need the data for the next page on the UI. Its a tabbeb page and all 3 needs the info that will be returned by the base class.
The problem I am having is that I need to call google api service for company info. Due to this I created a asynchronous call. So now the code returns due to this. Due to the tabbed page I need the data to bind to the screen and I now need to wait for the data.So basically need it to be synchronous.
I have tried everything I could find on this topic. Maybe the way I am doing this is wrong but hopefully my code will show that.
This is the Tabbed Page:
public BusinessTabbedPage(string PlaceId)
{
Children.Add(new BusinessSpecialsPage(PlaceId));
InitializeComponent();
}
This will be one of the pages on the app that calls the viewmodel
public BusinessSpecialsPage(string PlaceId)
{
BindingContext = new BusinessSpecialsPageViewModel(PlaceId);
InitializeComponent();
}
Due the 3 pages needing the same data I created a base class. This will get the data and pass everything back to the UI.
public BusinessBaseViewModel(string placeId)
{
Task<List<CompanyProfileModel>> task = GBDFromGoogle(placeId);
task.Wait();
}
public async Task<List<CompanyProfileModel>> GBDFromGoogle(string PlaceId)
{
var info = await ApiGoogle.GetGoogleCompanySelectedDetails(PlaceId);
var Companyresult = info.result;
CompanyProfileModel CompList = new CompanyProfileModel
{
ContactDetails = Companyresult.formatted_phone_number,
Name = Companyresult.name,
Website = Companyresult.website,
};
ComPF.Add(CompList);
return ComPF;
}
This is the api call which i think is adding a new task and then the process deadlocks?
public static async Task<GoogleSelectedPlaceDetails> GGCSD(string place_id)
{
GoogleSelectedPlaceDetails results = null;
var client = new HttpClient();
var passcall = "https://maps.googleapis.com/maps/api/place/details/json?placeid=" + place_id + "&key=" + Constants.GoogleApiKey;
var json = await client.GetStringAsync(passcall);
//results = await Task.Run(() => JsonConvert.DeserializeObject<GoogleSelectedPlaceDetails>(json)).ConfigureAwait(false);
results = JsonConvert.DeserializeObject<GoogleSelectedPlaceDetails>(json);
return results;
}
I need the process not to deadlock. It needs to wait for the tasks to be done so that I can return the data to the screen.
Task.Wait will block the current thread so you should never use it in Xamarin applications and even less in the constructor, otherwise your application will freeze until you receive some data which might never happen if the service is down or the user loses connection.
Instead of calling the data in your ViewModel's constructor you could initialize it on the Appearing event of your ContentPage view.
To do that, you could create a custom behaviour or even better, you can use the following library which already does this for you:
Using NuGet: Install-Package Behaviors.Forms -Version 1.4.0
Once you have installed the library, you can use the EventHandlerBehavior to wire events to ViewModel commands, for example:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:behaviors="clr-namespace:Behaviors;assembly=Behaviors"
xmlns:viewModels="clr-namespace:YourApp.ViewModels"
Title="Business Page"
x:Class="YourApp.Views.BusinessPage">
<ContentPage.BindingContext>
<viewModels:BusinessViewModel />
</ContentPage.BindingContext>
<ContentPage.Behaviors>
<behaviors:EventHandlerBehavior EventName="Appearing">
<behaviors:InvokeCommandAction Command="{Binding AppearingCommand}" />
</behaviors:EventHandlerBehavior>
</ContentPage.Behaviors>
[...]
The ViewModel:
public BusinessBaseViewModel(string placeId)
{
AppearingCommand = new Command(Appearing);
PlaceId = placeId;
}
public ICommand AppearingCommand { get; private set; }
public string PlaceId { get; private set; }
private ObservableCollection<CompanyProfileModel> _googleGbd;
public ObservableCollection GoogleGbd
{
get { _googleGbd?? (_googleGbd = new ObservableCollection<CompanyProfileModel>()); };
set
{
if (_googleGdb != value)
{
_googleGdb = value;
NotifyPropertyChanged();
}
}
}
private async void Appearing()
{
var companyResult = await ApiGoogle.GetGoogleCompanySelectedDetails(PlaceId);
CompanyProfileModel companyProfile = new CompanyProfileModel
{
ContactDetails = companyResult.formatted_phone_number,
Name = companyResult.name,
Website = companyResult.website,
};
GoogleGbd.Add(companyProfile);
}
If you only want the data to be loaded the first time your View appears, then you can add a bool flag to know that you have already loaded the data.
I'm building a C# UWP program (using MvvMLight framework) to run on a Raspberry Pi running Windows 10 Iot.
I have a method which downloads data from a SQL database via a WCF connection, I can see from debugging the WCF service is working, and the data downloads.
I'm puzzled why I can call this method two different ways, one updates the UI, one doesn't. Please can anyone explain why?
public class TagRegStep2ViewModel : ViewModelBase
{
private ObservableCollection<EmployeeName> _Employees;
private string _OnScreenInstructions;
public AsyncRelayCommand StartWizardCommand { get; private set; }
public TagRegStep2ViewModel()
{
// Initialise Messenger
Messenger.Default.Register<InitialiseStepMessage>(this, (msg) => InitialiseStep(msg.Step));
// Initialise Commands
StartWizardCommand = new AsyncRelayCommand(() => GetData(), () => true);
//Set dummy default value
OnScreenInstructions = "Default Value";
}
// I can see data is downloaded from WCF service and the ObservableCollection updates, but this isn't reflected in the UI.
public void InitialiseStep(ViewModelBase vm)
{
if (vm.GetType() == this.GetType())
{
Debug.WriteLine("InitialiseStep() called");
StartWizardCommand.TryExecute();
}
}
// Method that retrieves data from WCF service.
// When called from a relay command, it updates the ObservableCollection and UI.
// When called from a MessengerCommand it updates the ObservableCollection but not the UI. Why???
public async Task GetData()
{
Debug.WriteLine("Getting data...");
OnScreenInstructions = "Loading Employees";
await Task.Delay(25);
var emps = await LoadEmployees(); //not posted this method, but it works
Employees = new ObservableCollection<EmployeeName>(emps);
OnScreenInstructions = "Please select an employee.";
await Task.Delay(25);
Debug.WriteLine("ObservableCollection Count :" + Employees.Count);
}
public ObservableCollection<EmployeeName> Employees
{
get { return _Employees; }
set { Set(() => Employees, ref _Employees, value); }
}
public string OnScreenInstructions
{
get { return _OnScreenInstructions; }
set { Set(() => OnScreenInstructions, ref _OnScreenInstructions, value); }
}
}
I'm using the sqlite-net-pcl nuget, in my view model I am trying to get the list of announcements from my database
private SQLiteAsyncConnection connection;
public ObservableCollection<Announcement> AnnouncementList { get; private set; }
public AnnouncementsViewModel() {
connection = DependencyService.Get<ISQLiteDb>().GetConnection();
Initialize();
}
public async void Initialize() {
await connection.CreateTableAsync<Announcement>();
var announcements = await connection.Table<Announcement>().ToListAsync();
AnnouncementList = new ObservableCollection<Announcement>(announcements);
System.Diagnostics.Debug.WriteLine("***********************************");
System.Diagnostics.Debug.WriteLine(AnnouncementList.Count);
}
in my code behind, in the constructor:
BindingContext = new AnnouncementsViewModel();
InitializeComponent();
var list = (BindingContext as AnnouncementsViewModel).AnnouncementList;
The error I get is:
System.NullReferenceException: Object reference not set to an instance
of an object.
I put a break point in my viewModel, when it arrives to the first await it returns to the code behind and the App crashes. I get the null exception because the AnnouncementList is not filled in the viewModel and it didn't print the stars.
How can I solve this problem?
Thanks
You can't just call your async Initialize() method in a sync way like that. It will return immediately and by the time you get around to using AnnouncementList, Initialize isn't finished running yet and it is still null.
It's not an ideal solution but you should add .Wait() after Initialize() to ensure it completes before you exit the constructor.
I say 'not ideal' because depending on your context Wait() might block. If that happens you're probably better off doing that initialization work before constructing your ViewModel, using only 'proper' awaiting, and passing AnnouncementsList into the constructor.
The new ViewModel:
public AnnouncementsViewModel() {
connection = DependencyService.Get<ISQLiteDb>().GetConnection();
}
public async void GetAnnouncement() {
await connection.CreateTableAsync<Announcement>();
var announcements = await connection.Table<Announcement>().ToListAsync();
AnnouncementList = new ObservableCollection<Announcement>(announcements); System.Diagnostics.Debug.WriteLine("***********************************");
System.Diagnostics.Debug.WriteLine(AnnouncementList.Count);
}
Then In my code behind:
protected override async void OnAppearing() {
(BindingContext as AnnouncementsViewModel).GetAnnouncement();
if ((BindingContext as AnnouncementsViewModel).list != null)
classAnnouncementListView.ItemsSource = (BindingContext as AnnouncementsViewModel).list;
base.OnAppearing();
}
I have 2 classes and want to send object using Messenger while navigating from page to another and it works but only when navigate to the page and come back then try again not from first try.
ManivViewModel code:
public void GoToDetial(object parameter)
{
try
{
var arg = parameter as ItemClickEventArgs;
var item = arg.ClickedItem as Item;
Messenger.Default.Send<Item>(item, "Mess");
_navigationService.Navigate(typeof(DescriptionPage));
}
catch { }
}
DescriptionViewModel code:
public DescriptionViewModel(IRSSDataService rssService, INavigationService navigationService, IDialogService dialogService)
{
_dataService = rssService;
_navigationService = navigationService;
_dialogService = dialogService;
load();
LoadCommand = new RelayCommand(load);
GoToUrlCommand = new RelayCommand<object>(GoToUrl);
ShareSocialCommand = new RelayCommand(ShareSocial);
}
private void load()
{
Messenger.Default.Register<Item>(
this,
"Mess",
selectedItem =>
{
Item = selectedItem;
// Prepare content to share
RegisterForShare();
GetFromHTML(Item.Link);
});
}
I found it. I just need to pass in "true" to the Register call in the SimpleIoc to create the instance of the DescriptionViewModel immediately like this
SimpleIoc.Default.Register<DescriptionViewModel>(true);