How to PushAsync from MasterPage? - xamarin

I have MasterPage that currently using Event-Driven approach as follow:
MasterPage.xaml.cs
private void TapHome_Tapped(object sender, EventArgs e)
{
Detail = new NavigationPage(new HomePage());
IsPresented = false;
}
App.xaml.cs:
public App()
{
InitializeComponent();
if (!string.IsNullOrEmpty(Preferences.Get(Constant.Setting_AccessToken, "")))
{
MainPage = new MasterPage();
}
else if (string.IsNullOrEmpty(Preferences.Get(Constant.Setting_UserEmail, "")) &&
string.IsNullOrEmpty(Preferences.Get(Constant.Setting_Password, "")))
{
MainPage = new NavigationPage(new LoginPage());
}
}
Since we tried to migrate to the MVVM approach. We attempted to change the code:
MasterPage.xaml.cs
public MasterPage()
{
InitializeComponent();
BindingContext = _viewModel = new MasterViewModel()
{
Navigation = Navigation
};
//IsPresented = false;
}
MasterViewModel.cs:
public ICommand HomeCommand { get; private set; }
public ICommand ActivitiesCommand { get; private set; }
public ICommand ChangePasswordCommand { get; private set; }
public ICommand LogoutCommand { get; private set; }
public MasterViewModel()
{
HomeCommand = new Command(async () => await GoToHome());
ActivitiesCommand = new Command(async () => await GoToActivities());
ChangePasswordCommand = new Command(async () => await GoToChangePassword());
LogoutCommand = new Command(async () => await Logout());
}
public async Task GoToHome()
{
// Attempt 1:
// Navigation.PushAsync(new NavigationPage(new HomePage()));
// Attempt 2:
// Application.Current.MainPage = new NavigationPage(new HomePage());
}
Tried the first attempt and we have error on Android - System.InvalidOperationException: PushAsync is not supported globally on Android, please use a NavigationPage.
Tried the second attempt and worked OK but no navigation at HomePage or other pages after clicking it.
Any ideas?

Never mind. We worked out this:
public async Task GoToHome()
{
((MasterDetailPage)Application.Current.MainPage).Detail = new NavigationPage(new HomePage());
((MasterDetailPage)Application.Current.MainPage).IsPresented = false;
}

Related

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

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));
}

Xamarin Forms updating map pins when new pin added

I have a Map in Xamarin Forms that will show a collection of Pins stored in a SQLite database. Its quite simple, the map shows with the pins, there is an Add button at the top which presents with a textbox to give a new pin a Name. The Lat/Lng is populated automatically and when Save is pressed, the new pin should get added to the map.
When the map loads, the pins are not shown on the map. I have created a button to load the pins and they will show when that button is pressed.
A breakpoint in the ViewModel ExecuteLoadPinsCommand() shows that Pins does have pins inside it.
When I create a new pin, again the new pin doesn't show.
Here is the View. Note the toolbar button with the binding Pins.Count, thats the button I click to show the pins.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps"
x:Class="WE.Views.MapPage"
xmlns:vm="clr-namespace:WE.ViewModels"
Title="{Binding Title}">
<ContentPage.BindingContext>
<vm:MapPageViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Text="Add" Command="{Binding AddPinCommand}" />
<ToolbarItem Text="{Binding Pins.Count}" Clicked="ToolbarItem_Clicked" />
</ContentPage.ToolbarItems>
<ContentPage.Content>
<maps:Map x:Name="myMap" ItemsSource="{Binding Pins}" IsShowingUser="True">
<maps:Map.ItemTemplate>
<DataTemplate>
<maps:Pin Label="{Binding Name}" Position="{Binding Position}"/>
</DataTemplate>
</maps:Map.ItemTemplate>
</maps:Map>
</ContentPage.Content>
Here is the ViewModel.
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MapPage : ContentPage
{
MapPageViewModel _viewModel;
public MapPage()
{
InitializeComponent();
BindingContext = _viewModel = new MapPageViewModel();
_viewModel.LoadPinsCommand.Execute(this);
}
protected override async void OnAppearing()
{
base.OnAppearing();
_viewModel.OnAppearing();
await GetCurrentLocation();
}
CancellationTokenSource cts;
async Task GetCurrentLocation()
{
var request = new GeolocationRequest(GeolocationAccuracy.Medium, TimeSpan.FromSeconds(10));
cts = new CancellationTokenSource();
var location = await Geolocation.GetLocationAsync(request, cts.Token);
if (location != null)
{
Console.WriteLine($"Latitude: {location.Latitude}, Longitude: {location.Longitude}, Altitude: {location.Altitude}");
myMap.MoveToRegion(MapSpan.FromCenterAndRadius(new Position(location.Latitude, location.Longitude), Distance.FromMeters(100)));
}
}
private async void ToolbarItem_Clicked(object sender, EventArgs e)
{
UpdatePins();
}
void UpdatePins()
{
foreach (var pin in _viewModel.Pins)
{
Pin newPin = new Pin
{
Label = pin.Name,
Position = new Position(Convert.ToDouble(pin.Latitude), Convert.ToDouble(pin.Longitude)),
Type = PinType.Place
};
myMap.Pins.Add(newPin);
}
}
}
and finally, here is the View Model
public class MapPageViewModel : BaseViewModel
{
private readonly IMapService _mapService;
public int PinCount { get; set; }
public ObservableCollection<MapPinModel> Pins { get; set; }
public Command LoadPinsCommand { get; }
public Command AddPinCommand { get; }
public MapPageViewModel()
{
_mapService = new MapService();
Title = "Map";
Pins = new ObservableCollection<MapPinModel>();
PinCount = 0;
LoadPinsCommand = new Command(async () => await ExecuteLoadPinsCommand());
AddPinCommand = new Command(OnAddPin);
this.LoadPinsCommand.Execute(null);
}
async Task ExecuteLoadPinsCommand()
{
IsBusy = true;
try
{
Pins.Clear();
Pins = await App.Database.GetPins();
Debug.WriteLine("Pins in DB: " + Pins.Count.ToString());
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
public async void OnAppearing()
{
IsBusy = true;
}
private async void OnAddPin(object obj)
{
await Shell.Current.GoToAsync(nameof(AddPinPage));
}
}
Add New Pin
ViewModel
private readonly IMapService _mapService;
private string userid;
private string latitude;
private string longitude;
private DateTime dateadded = DateTime.Now;
private DateTime datefound = DateTime.Now;
private string name;
public AddPinViewModel()
{
SaveCommand = new Command(OnSave, ValidateSave);
CancelCommand = new Command(OnCancel);
this.PropertyChanged +=
(_, __) => SaveCommand.ChangeCanExecute();
_mapService = new MapService();
}
private bool ValidateSave()
{
return !String.IsNullOrWhiteSpace(name);
}
public string UserId
{
get => userid;
set => SetProperty(ref userid, value);
}
public string Latitude
{
get => latitude;
set => SetProperty(ref latitude, value);
}
public string Longitude
{
get => longitude;
set => SetProperty(ref longitude, value);
}
public DateTime DateAdded
{
get => dateadded;
set => SetProperty(ref dateadded, value);
}
public DateTime DateFound
{
get => datefound;
set => SetProperty(ref datefound, value);
}
public string Name
{
get => name;
set => SetProperty(ref name, value);
}
public Command SaveCommand { get; }
public Command CancelCommand { get; }
private async void OnCancel()
{
// This will pop the current page off the navigation stack
await Shell.Current.GoToAsync("..");
}
private async void OnSave()
{
var location = await GetCurrentLocation();
var userid = await Xamarin.Essentials.SecureStorage.GetAsync("userId");
MapPinModel pin = new MapPinModel()
{
Name = Name,
DateAdded = DateTime.Now,
DateFound = DateTime.Now,
Latitude = location.Latitude,
Longitude = location.Longitude
};
await App.Database.AddPin(pin);
await Shell.Current.GoToAsync("..");
}
CancellationTokenSource cts;
async Task<LatLngModel> GetCurrentLocation()
{
LatLngModel model = new LatLngModel();
var request = new GeolocationRequest(GeolocationAccuracy.Medium, TimeSpan.FromSeconds(10));
cts = new CancellationTokenSource();
var location = await Geolocation.GetLocationAsync(request, cts.Token);
if (location != null)
{
model.Latitude = location.Latitude.ToString();
model.Longitude = location.Longitude.ToString();
}
return model;
}

Xamarin Forms Map not adding pins on new pin created

I have a Xamarin Forms map that shows a collection of Pins. When I add a new location it doesn't show the newly created pin until I click 'Refresh'.
I originally had the AddLocation subscribe in the constructor of the ViewModel and it worked, but added the pin 3 times to the database and map. Its since moving the AddLocation subscribe and unsubscribe to their own methods in the ViewModel and calling then OnAppearing() and OnDisappearing() that the new pin doesnt show.
The map is on a page called ItemPage.xaml
<ContentPage.ToolbarItems>
<ToolbarItem Text="Add" Clicked="ToolbarItem_Clicked" />
<ToolbarItem Text="Refresh" Clicked="ToolbarItem_Clicked_1" />
</ContentPage.ToolbarItems>
<ContentPage.Content>
<!--The map-->
<ContentView Content="{Binding Map}" />
</ContentPage.Content>
and the Code Behind -
[DesignTimeVisible(false)]
public partial class ItemPage : ContentPage
{
ItemViewModel viewModel;
public ItemPage()
{
InitializeComponent();
BindingContext = viewModel = new ItemViewModel();
}
private void Button_Clicked(object sender, EventArgs e)
{
}
protected override void OnAppearing()
{
base.OnAppearing();
}
async void ToolbarItem_Clicked(object sender, EventArgs e)
{
await Navigation.PushModalAsync(new NavigationPage(new NewLocationPage()));
}
private void ToolbarItem_Clicked_1(object sender, EventArgs e)
{
viewModel.LoadLocationsCommand.Execute(null);
}
}
The View Model -
public class ItemViewModel : BaseViewModel
{
public ObservableCollection<MapPinModel> Locations { get; set; }
public ObservableCollection<ItemModel> Items { get; set; }
public Command LoadLocationsCommand { get; set; }
public Command LoadItemsCommand { get; set; }
public Map Map { get; private set; }
public ItemViewModel()
{
Title = "Items";
Locations = new ObservableCollection<MapPinModel>();
Items = new ObservableCollection<ItemModel>();
LoadLocationsCommand = new Command(async () => await ExecuteLoadLocationsCommand());
LoadItemsCommand = new Command(async () => await ExecuteLoadItemsCommand());
Map = new Map(MapSpan.FromCenterAndRadius(
new Xamarin.Forms.Maps.Position(53.70251232638285, -1.8018436431884768),
Distance.FromMiles(0.5)))
{
IsShowingUser = true,
VerticalOptions = LayoutOptions.FillAndExpand
};
this.LoadLocationsCommand.Execute(null);
}
public void UnsubscribeMessages()
{
MessagingCenter.Unsubscribe<NewLocationPage, ItemLocationModel>(this, "AddLocation");
}
public void SubscribeMessages()
{
MessagingCenter.Subscribe<NewLocationPage, ItemLocationModel>(this, "AddLocation", async (obj, item) =>
{
await ItemDataStore.AddItemLocationAsync(item);
});
}
async Task ExecuteLoadItemsCommand()
{
if (IsBusy)
return;
try
{
Items.Clear();
var items = await ItemDataStore.GetItemsAsync(true);
foreach (var item in items)
{
Items.Add(item);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
async Task ExecuteLoadLocationsCommand()
{
Map.Pins.Clear();
var locations = await ItemDataStore.GetLocationsAsync(true);
foreach (Feature feature in locations.Features)
{
if (!feature.Geometry.Type.Equals(GeoJSONObjectType.Point))
continue;
Point point = feature.Geometry as Point;
GeoJSON.Net.Geometry.Position s = point.Coordinates as GeoJSON.Net.Geometry.Position;
MapPinModel pin = new MapPinModel
{
PopupContent = feature.Properties["popupContent"].ToString(),
ScientificName = feature.Properties["scientificname"].ToString(),
ItemDescription = feature.Properties["description"].ToString(),
AddedBy = feature.Properties["username"].ToString(),
Latitude = s.Latitude.ToString(),
Longitude = s.Longitude.ToString()
};
Xamarin.Forms.Maps.Position position = new Xamarin.Forms.Maps.Position(Convert.ToDouble(pin.Latitude), Convert.ToDouble(pin.Longitude));
Pin newPin = new Pin
{
Label = pin.PopupContent,
Type = PinType.Place,
Position = position
};
Map.Pins.Add(newPin);
}
}
}
Finally the New Location page -
[DesignTimeVisible(false)]
public partial class NewLocationPage : ContentPage
{
ItemViewModel viewModel;
public ItemLocationModel Location { get; set; }
public NewLocationPage()
{
InitializeComponent();
Location = new ItemLocationModel();
BindingContext = viewModel = new ItemViewModel();
BindingContext = this;
viewModel.SubscribeMessages();
}
async void Save_Clicked(object sender, EventArgs e)
{
var location = await Geolocation.GetLastKnownLocationAsync();
Location.Latitude = location.Latitude.ToString();
Location.Longitude = location.Longitude.ToString();
Location.DatePosted = DateTime.Now;
Location.ItemId = ((ItemModel)itemsPicker.SelectedItem).Id;
Location.SecretLocation = secretLocation.IsToggled;
MessagingCenter.Send(this, "AddLocation", Location);
await Navigation.PopModalAsync();
Location = null;
}
async void Cancel_Clicked(object sender, EventArgs e)
{
await Navigation.PopModalAsync();
}
protected override void OnAppearing()
{
viewModel.SubscribeMessages();
base.OnAppearing();
if (viewModel.Items.Count == 0)
viewModel.LoadItemsCommand.Execute(null);
itemsPicker.ItemsSource = viewModel.Items;
}
protected override void OnDisappearing()
{
viewModel.LoadLocationsCommand.Execute(null);
viewModel.UnsubscribeMessages();
base.OnDisappearing();
}
}

XamarinForms: how use a Custom Navigation Page

I order to have a cutom navigation on a specific view on my app,
I create a Custom Navigation ios:
public class CustomNavigationPageRenderer : NavigationRenderer
{
public override void ViewDidLoad()
{
base.ViewDidLoad();
UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default);
UINavigationBar.Appearance.ShadowImage = new UIImage();
UINavigationBar.Appearance.BackgroundColor = UIColor.Clear;
UINavigationBar.Appearance.TintColor = UIColor.White;
UINavigationBar.Appearance.BarTintColor = UIColor.Clear;
UINavigationBar.Appearance.Translucent = true;
}
}
and a common interface class:
public partial class CustomPage : NavigationPage
{
public CustomPage(): base()
{
InitializeComponent();
}
public CustomPage(Page root) : base(root)
{
InitializeComponent();
}
public bool IgnoreLayoutChange { get; set; } = false;
protected override void OnSizeAllocated(double width, double height)
{
if (!IgnoreLayoutChange)
base.OnSizeAllocated(width, height);
}
}
Now, In my specific view, how have I use it?
I need to set on false the original navigation? (NavigationPage.SetHasNavigationBar(this, false);​)
public MySpecificViewNeedCustoNAv()
{
CustomPage myNavigationPage = new CustomPage();
...
need to set on false the original navigation?
No , you can refer the following code.
For example
Creating the Root Page in App.cs
public App ()
{
MainPage = new CustomPage (new xxxpage());
}
Pushing Pages to the Navigation Stack in your page
async void ButtonClicked (object sender, EventArgs e)
{
await Navigation.PushAsync (new xxxpage());
}

Xamarin forms navigation BarBackgroundColor issue

When I click on Login for the first time than the barbackground of mainpage becomes the right color, but when I logout and than login again the color of the barbackground doesn't change??
LoginPage (View)
public partial class LoginPage : ContentPage
{
LoginPageViewModel vm;
public LoginPage ()
{
vm = new LoginPageViewModel ();
BindingContext = vm;
InitializeComponent ();
}
public void OnClickLogin(object o, EventArgs e)
{
vm.Login ();
}
public void OnClickPasswoordVergeten(object o, EventArgs e)
{
vm.PasswoordVergeten ();
}
public void OnClickContactUs(object o, EventArgs e)
{
vm.ContactUs ();
}
}
LoginPageViewModel (ViewModel)
public class LoginPageViewModel: INotifyPropertyChanged
{
public LoginPageViewModel ()
{
}
public void Login()
{
App.Current.MainPage = new Dharma.MainPage();
}
}
MainPage (View)
public partial class MainPage : MasterDetailPage
{
public MainPage ()
{
InitializeComponent ();
masterPage.ListView.ItemSelected += OnItemSelected;
var page = new NavigationPage (new ListPage());
page.BarBackgroundColor = Color.FromRgb(26,179,148);
Detail = page;
masterPage.ListView.SelectedItem = null;
IsPresented = false;
}
void OnItemSelected (object sender, SelectedItemChangedEventArgs e)
{
var item = e.SelectedItem as MasterPageItem;
if (item != null) {
var page = new NavigationPage ((Page)Activator.CreateInstance (item.TargetType));
page.BarBackgroundColor = Color.FromRgb(26,179,148);
Detail = page;
masterPage.ListView.SelectedItem = null;
IsPresented = false;
}
}
}
Set the color to your master page.
public void Login()
{
var mainPage = new Dharma.MainPage();
mainPage.BarBackgroundColor = Color.FromRgb(26,179,148);
App.Current.MainPage = mainPage ;
}

Resources