ViewModel's properties become null when navigating - xamarin

I have two Pages, which share the same ViewModel. I did this in order to handle the same data when navigating from Page1 to Page2.
The data that I need when I navigate to Page2 is the text of Page 1 entries, which are binded to the ViewModel. Page1 constructor:
public Page1()
{
InitializeComponent();
this.BindingContext = new ViewModel(this.Navigation);
}
In XAML of Page1:
<Entry Text="{Binding Entry1TextFromPage1}"/>
<Entry Text="{Binding Entry2TextFromPage1}"/>
In the ViewModel I do:
public ViewModel(INavigation navigation)
{
this.Navigation = navigation;
this.ButtonOnPage1Command = new Command( async () =>
await this.Navigation.PushAsync(new Page2());
);
this.ButtonOnPage2Command = new Command( async () =>
Use(this.Entry1TextFromPage1); //Here the text is null
Use(this.Entry2TextFromPage1); //Here the text is null
);
}
All the binded text entries (from Page1) become null. What am I missing?

Your Page2 probably don't have its BindingContext set to the same ViewModel. Pass your ViewModel as a parameter to your Page2 constructor, so you'd call it like this PushAsync(new Page2(this)).
public Page2(ViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}

Related

UI freezes for 2-3 seconds when loading a carouselview

I have a carouselview binded to a viewmodel, on the previous page (call it first page) user can select 2 arguments and with the help of those, the next view (call it second page) is generated accordingly. However, I can't wrap my head around why my view won't load asynchronously.
So my problem: When I click the button on the first page the UI would freeze for like a solid 2-3 seconds, and then start load (asynchronously?) and once it's done it's all good.
Also I couldn't really figure out a better way to inherit values from first page to second so if someone has an idea please let me know.
Any help on how can I fix this I would really appreciate.
The viewmodel for second page
public class DataSelectionViewModel : BaseViewModel
{
public ObservableCollection<Items> FilteredData { get; set; }
public UserSelectionViewModel()
{
_dataStore = DependencyService.Get<IDataStore>();
LoadData= new AsyncAwaitBestPractices.MVVM.AsyncCommand(FilterData);
FilteredData = new ObservableRangeCollection<Items>();
}
public async Task FilterData()
{
FilteredData.Clear();
var filtereddata = await _dataStore.SearchData(Hard, Subject).ConfigureAwait(false);
foreach (var data in filtereddata)
{
FilteredData.Add(data);
}
}
}
The carouselview in second page
<ContentPage.BindingContext>
<db:DataSelectionViewModel/>
</ContentPage.BindingContext>
...
<!-- DataTemplate for carouselview has radiobuttons, label and button all in a grid -->
<CarouselView ItemsSource="{Binding FilteredData}">
Second Page c#
public partial class SecondPage : ContentPage
{
public Coll(bool hard, string subject)
{
InitializeComponent();
var vm = (BaseViewModel)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 first page view containing the button
<Button x:Name="Start" Pressed="ButtonClick"/>
First page c# --> Here I also tried doing it with a command and a pressed at the same time, because I couldn't come up with a way to save variables to second page viewmodel, that's why I use pressed here
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));
}
I have tried not using the OnAppearing method to get my data, but then it wouldn't bind to the page and it would not show, if I were to previously fill the ObservableCollection with my data and then load the page although I would love to be able to do this because it would allow me to create a loading popup also.

Using a view model and calling OnAppearing in ContentView

I'm using view models for my ContentPage's in my Xamarin Forms 5 app and typically call an Init() method in my view model from the OnAppearing() method in code behind.
I tried the same approach in my ContentView but it's never hitting the OnAppearing() method.
This is my ContentView code:
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MyApp.ViewModels"
x:Class="MyApp.MyContentView">
<ContentView.BindingContext>
<vm:MyViewModel/>
</ContentView.BindingContext>
<ContentView.Content>
<StackLayout
BackgroundColor="{StaticResource PrimaryDark }"
HeightRequest="200">
<Label
Text="{Binding User.FullName}"
TextColor="White"
FontSize="Medium"
FontAttributes="Bold"
HorizontalOptions="CenterAndExpand"/>
</StackLayout>
</ContentView.Content>
</ContentView>
The view model for this content view looks like this:
public class MyViewModel : BaseViewModel
{
User user;
public MyViewModel()
{
}
public User User
{
get => user;
set
{
if (user == value)
return;
user = value;
OnPropertyChanged();
}
}
public async void Init()
{
// Get user info
var data = await _dbService.GetUser();
if(data != null)
{
User = data;
OnPropertyChanged(nameof(User));
}
}
}
And in my code behind, this is what I'm doing:
public partial class MyContentView : ContentView
{
MyViewModel _vm;
public MyContentView()
{
InitializeComponent();
_vm = new MyViewModel();
BindingContext = _vm;
}
protected virtual void OnAppearing()
{
_vm.Init();
}
}
This pattern is working nicely in my content pages but not working in a content view. What am I doing wrong here?
The content view doesn't have the lifecycle methods like the content page. So when the content view shows or displays on the screen, the OnAppearing() and OnDisAppearing method developer custom will not invoke.
So you can call the the page's OnAppearing() method to do that if there is only a content view in your page. And if there is not only one contentview, you can call the _vm.Init(); method when you use the instance of the content view.
Here's what I've done and it seems to be working fine.
First, I created a ContentView to display the flyout header which includes user's avatar and name. Notice that I set the view model for this content view in the XAML file -- see below:
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
xmlns:vm="clr-namespace:MyApp.ViewModels"
x:Class="MyApp.Views.FlyoutHeader">
<ContentView.BindingContext>
<vm:AppViewModel/>
</ContentView.BindingContext>
<ContentView.Content>
<StackLayout
BackgroundColor="{StaticResource PrimaryDark }"
HeightRequest="200">
<xct:AvatarView
Source="{Binding UserInfo.AvatarUrl}"
Size="100"
HorizontalOptions="CenterAndExpand"
VerticalOptions="CenterAndExpand"/>
<Label
Text="{Binding UserInfo.FullName}"
TextColor="White"
FontSize="Medium"
FontAttributes="Bold"
HorizontalOptions="CenterAndExpand"
Margin="0,0,0,30"/>
</StackLayout>
</ContentView.Content>
</ContentView>
I then created a view model named AppViewModel that I intend to use in multiple places, including the FlyoutHeader.xaml that I shared above. Here's what AppViewModel looks like:
public class AppViewModel : BaseViewModel
{
User user { get; set; }
public AppViewModel()
{
}
public User UserInfo
{
get => user;
set
{
if (user == value)
return;
user = value;
OnPropertyChanged();
}
}
public async void Init()
{
if(user == null || user.Id == Guid.Empty)
{
var data = await _dbService.GetUser();
if(data != null)
{
UserInfo = data;
OnPropertyChanged();
}
}
}
}
Finally, in the code behind for FlyoutHeader.xaml.cs, I call the Init() method of the view model in the constructor:
public partial class FlyoutHeader : ContentView
{
AppViewModel _vm;
public FlyoutHeader()
{
InitializeComponent();
_vm = new AppViewModel();
_vm.Init();
BindingContext = _vm;
}
}
I'm actually a bit concerned that there maybe tight coupling with the UI and the async call being initiated in the constructor may tie up the UI thread and delay it. Please let me know if there's a better way to handle this.

Displaying authenticated user info on AppShell FlyoutHeaderTemplate

I'm trying to create a flyout header that displays the authenticated user's avatar and name in the FlyoutHeaderTemplate.
The issue I ran into is that it looks like OnAppearing() method is not invoked at all for AppShell. I put break points in that method and never hit it.
I also tried using a ContentView to display the header in my Flyout but turns out ContentView's don't have OnAppearing() or OnDisappearing() lifecycle events either.
How do I display authenticated user info in my flyout? I have the user info in my local database. All I need to do is:
Be able to call var user = await _dbService.GetUser(); to read user info
I also need to be able to bind to certain controls i.e. Label for username and Avatar for user image.
Here's what I have in my AppShell:
<Shell.FlyoutHeaderTemplate>
<DataTemplate>
<StackLayout>
<Label Text="{Binding ???}"/>
</StackLayout>
</DataTemplate>
</Shell.FlyoutHeaderTemplate>
First time you run the Appshell it will hit the constructor so you can call var user = await _dbService.GetUser(); in the constructor . Then Bind the shell to itself this.BindingContext = this;. Add a string property to bind it to the Text Label in xaml.
private string name;
public string Name { get{return name;} set { SetProperty(ref name, value); }
public AppShell()
{
InitializeComponent();
Task.Run(async () =>
{
await GetUser();
});
this.BindingContext = this;
}
public async void GetUser(){
var user = await _dbService.GetUser();
Name = user.Name;
}
The OnAppearing() will hit whenever you go to await Shell.Current.GoToAsync("..");. So you can call var user = await _dbService.GetUser(); in it also.

Xamarin: Updating an ObservableCollection in the Background

I have a Xamarin.Forms Application based on a Listview populated by an ObservableCollection<Item> and bound to a SQLite model exposed as List<Item>.
Now I have difficulties figuring out how I can update the data via the web.
The whole process is supposed to run in the background. If everything runs as desired, the update process would be based on async ... await tasks and new items would appear one by one on the screen as they are pulled in.
Would anyone please guide me how to lay out my application and how to implement such a background update task?
Notes:
More experienced colleagues warned me that such a concept cannot be done in Xamarin at all, since, so they say, ObservableCollection "does not support to be updated by a background task". I did some research on this, and found indeed some indication that this could be true, but the infos were long outdated (from 2008), things have very likely changed since then.
For performance reasons I cannot simply pull in the complete list and throw away the existing list, but I need to implement a record based update looking at the items one by one. To accomplish this records have an unique Id, and are timestamped. My app knows when it has last seen the web service, and can query all items which have changed since then. I already have a REST service pulling in the changed Items data from the backend as a List, but cannot figure out how to refresh the ObservableCollection and the underlying database.
I do all my updates on change to the ListView. Here I have a button in the a list view which when clicked updates a property which persists by saving it to the sql database. It assumes you have your database set up.
Database:
Function which updates item if exist or saves new. This is a Task so can be called asynchronously.
public Task<int> SaveItemAsync(Item item)
{
if(item.ItemId != 0)
{
return database.UpdateAsync(item);
}
else
{
return database.InsertAsync(itme);
}
}
Xaml
List View which binds to an Observable collection created from the item database.
GestureRecognizers is set up on the image and is bound to a tapCommand in the ViewModel - The code behind the Xaml defines the binding context
Code behind
ItemViewModel viewModel;
public MessagePage ()
{
InitializeComponent ();
viewModel = new ItemViewModel();
BindingContext = viewModel;
}
And then the Xaml
Bind to the ObsevableCollection "ItemsPassed" and this set as the binding context within it. As a result you need to go back to the
page BindingContext so note the binding path for the TapCommand.
Pass the ItemId through as a parameter
<ListView ItemsSource="{Binding ItemsPassed}"
HasUnevenRows="True"
x:Name="ItemListView">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ContentView>
<StackLayout Padding="20">
<StackLayout Orientation="Horizontal">
<StackLayout Orientation="Vertical" HorizontalOptions="Start">
<Label Text="{Binding ItemText}"
FontAttributes="Bold"/>
</StackLayout>
<Image Source="{Binding Favourited, HeightRequest="12" HorizontalOptions="EndAndExpand">
<Image.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Path=BindingContext.TapCommand, Source={x:Reference ItemListView}}"
CommandParameter="{Binding ItemId}"/>
</Image.GestureRecognizers>
</Image>
</StackLayout>
</StackLayout>
</ContentView>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
ViewModel
Define the ICommand and assign a function to it
Find the item in the observable collection and change the property
This also needs to be changed in the database using the await App.Database.SaveItemAsync(item) - because of this the function called of the command must be async
public class ItemsViewModel : INotifyPropertyChanged
{
public ObservableCollection<Item> ItemsPassed { get; set; } = new ObservableCollection<Item>();
// Get items out of yourdatabase
public async void GetItems()
{
List<Item> itemsOut = await App.Database.GetItemsAsync();
foreach (Item i in itemsOut)
{
ItemsPassed.Add(i);
}
}
ICommand tapCommand;
public ItemsViewModel()
{
GetItems();
tapCommand = new Command(ExecuteTap);
}
public ICommand TapCommand
{
get { return tapCommand; }
}
// Find the item selected and change the property
private async void ExecuteTap(object obj)
{
var item = ItemsPassed.FirstOrDefault(i => i.ItemId.ToString() == obj.ToString());
if (item != null)
{
if (item.Favourited == true)
{
item.Favourited = false;
}
else
{
item.Favourited = true;
}
await App.Database.SaveItemAsync(item);
Console.WriteLine("update the database");
}
}
}
You then want to make sure the changes occur in the view - this is done through extending INotifyPropertyChange in the Model and calling it when the property changes.
Model
When the item is changed in the viewmodel the OnPropertyChanged is fired which cause the view to update.
public class Item : INotifyPropertyChanged
{
[PrimaryKey, AutoIncrement]
public int ItemId { get; set; }
public string ItemText { get; set; }
public string Author { get; set; }
private bool _favourited;
public bool Favourited
{
get { return _favourited; }
set
{
_favourited = value;
OnPropertyChanged("Favourited");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Hope this helps. Sorry I had to put things in snippets the code section wasn't working properly probably because some of the code is wrong, but its just for example.

Navigation on ViewModel in MVVM Light in Xamarin.Forms

I have a Xamarin.froms app in MVVM Light, i want to open another page on button click, but i only want to use this method of Page class
page.Navigation.PushModalAsync(new MyPage())
How to send "Page" class as reference on ViewModel class.
There are variety of ways to do this.
Easy way:
Pass the Page as a constructor parameter of View Model.
The useful way:
Write a Navigation Service. This can extend from a Content Page like this:
public class NavigationService : ContentPage
{
public static INavigation Navigation
{
get {
return Application.Current.MainPage.Navigation;
}
}
public static IReadOnlyList<Page> NavigationStack () {
return Navigation.NavigationStack;
}
}
Now you can use this Service in your ViewModel like this:
Page lastPage = Navigation.NavigationStack.Last;
XAML code
<Button Text="Help" Command="{Binding NavigateHelpPage}"/>
MVVM Hierarchical page navigation in Xamarin.forms
public ICommand NavigateHelpPage { get; }
private async Task NavigateToHelpPage()
{
await Application.Current.MainPage.Navigation.PushAsync(new HelpPage());
}
Inside Constructor:
NavigateHelpPage = new Command(async () => await NavigateToHelpPage());
MainPage you can set like :
Application.Current.MainPage = new NavigationPage(new Views.Dashboard.DashboardPage());

Resources