OnAppearing event firing twice .with tabbed page - xaml

I am facing issue being a beginner for Xamarin forms and MVVM . I have tabbedpage and 2 pages are under tag . Here is code.
Issue is local:ActiveOrderViewPage page OnAppearing() event is firing twice when tabbedPage is loading and execute twice code under OnAppearing() event .
Please help me to find this why this is happening ?
Here Is code Tabbed Page
tabbedpage.xaml
<TabbedPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
Title="Orders">
<TabbedPage.Children>
<local:ActiveOrderViewPage />
<local:SavedOrderViewPage />
</TabbedPage.Children>
tabbedpage.xaml.cs
public partial class OrderTabViewPage : TabbedPage
{
public OrderViewModel ViewModel { get { return BindingContext as OrderViewModel; } }
public OrderTabViewPage()
{
InitializeComponent();
this.BindingContext = ViewModelLocator.OrderVModel;
}
public OrderTabViewPage(params object[] arg) : this()
{
if (arg != null)
{
ViewModel.AccountID = Convert.ToInt32(arg[0]);
}
}
Here is active order .cs
public partial class ActiveOrderViewPage : ContentPage
{
public OrderViewModel ViewModel { get { return BindingContext as OrderViewModel; } }
public ActiveOrderViewPage()
{
InitializeComponent();
this.BindingContext = ViewModelLocator.OrderVModel;
}
//public OrderViewPage() : this()
//{
// ViewModel.AccountID = accuntId;
//}
protected override void OnAppearing()
{
base.OnAppearing();
if (ViewModelLocator.OrderVModel.ActiveOrderItems == null || ViewModelLocator.OrderVModel.ActiveOrderItems.List.Count == 0)
{
ViewModelLocator.OrderVModel.ActiveOrderCommand.Execute(null);
}
}
Thanks in advance ...

Having had this problem for a long long time, before realising, I know how frustrating this is. The event OnAppearing() fires twice because of the way that the tabbed page renders all of the individual pages. It initially renders the page, then in your case will render the other page, which causes the OnDisappearing() to fire. The first page then gets focus, causing the OnAppearing() to fire again.
If you only want the code to fire once, after adding the child pages, set the currentpage property to null, which should stop the OnAppearing() from firing again

In my case OnAppearing called twice while i had in app.xaml the follow code:
MainPage = new NavigationPage(new MainPage());
after i change the code to:
MainPage =new MainPage();
The OnAppearing is no more calling twice.

Related

.Net Maui Shell Navigation - Is it possible to pass a Query Parameter and Auto Populate a Page?

I need to auto populate a Page by passing a Shell Navigation Parameter to a ViewModel/Method and call a Service to return a single record from a Web Service. Essentially a drill-through page. My issue is that I need to call the data retrieveal command, "GetFieldPerformanceAsync" (note [ICommand] converts this to "GetFieldPerformanceCommand") from the "To" Page's code-behind from within OnNavigatedTo. This is required since the Shell Navigation Parameter is not set in the ViewModel until the Page is loaded. I'm currently unable to make the Command call from OnNavigatedTo and need advice on how to accomplish this.
Thanks!
Code behind the Page:
public partial class FieldPerformancePage : ContentPage
{
public FieldPerformancePage(FieldPerformanceViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
//works with parameter hard-coded in ViewModel
//viewModel.GetFieldPerformanceCommand.Execute(null);
}
FieldPerformanceViewModel viewModel;
protected override void OnNavigatedTo(NavigatedToEventArgs args)
{
base.OnNavigatedTo(args);
//this does not work
viewModel.GetFieldPerformanceCommand.Execute(null);
}
}
ViewModel
namespace TrackMate.ViewModels;
[QueryProperty(nameof(FieldAssignedWbs), nameof(FieldAssignedWbs))]
public partial class FieldPerformanceViewModel : BaseViewModel
{
[ObservableProperty]
FieldAssignedWbs fieldAssignedWbs;
[ObservableProperty]
FieldPerformance fieldPerformance;
FieldPerformanceService fieldPerformanceService;
public FieldPerformanceViewModel(FieldPerformanceService fieldStatusService)
{
Title = "Status";
this.fieldPerformanceService = fieldStatusService;
}
[ICommand]
async Task GetFieldPerformanceAsync()
{
if (IsBusy)
return;
try
{
IsBusy = true;
int wbsId = fieldAssignedWbs.WbsId;
var fieldPerformanceList = await fieldPerformanceService.GetFieldPerformanceList(wbsId);
if (fieldPerformanceList.Count != 0)
FieldPerformance = fieldPerformanceList.First();
}
catch (Exception ex)
{
Debug.WriteLine(ex);
await Shell.Current.DisplayAlert("Error!",
$"Undable to return records: {ex.Message}", "OK");
}
finally
{
IsBusy = false;
}
}
}
I believe I figured it out...
By adding ViewModel Binding within the OnNavigatedTo method in the "DetailsPage" Code Behind, a Command Call can be made to the Page's ViewModel to execute data retrieval method after the Shell Navigation Parameter (object in this scenario) passed from the "Main" Page has been set. Note a null is passed since the Query Parameter is sourced from the ViewModel. If you are new to .Net Maui, as I am, I recommend James Montemagno's video on .Net Maui Shell Navigation.
namespace TrackMate.Views;
public partial class FieldPerformancePage : ContentPage
{
public FieldPerformancePage(FieldPerformanceViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
protected override void OnNavigatedTo(NavigatedToEventArgs args)
{
FieldPerformanceViewModel viewModel = (FieldPerformanceViewModel)BindingContext;
viewModel.GetFieldPerformanceCommand.Execute(null);
base.OnNavigatedTo(args);
}
}
For me it only worked when the BindingContext assignment is before the component initialization and the method call after the base call in OnNavigatedTo
public partial class OccurrencePage : ContentPage
{
public OccurrencePage(OccurrenceViewModel model)
{
BindingContext = model;
InitializeComponent();
}
protected override void OnNavigatedTo(NavigatedToEventArgs args)
{
base.OnNavigatedTo(args);
OccurrenceViewModel viewModel = (OccurrenceViewModel)BindingContext;
viewModel.GetFieldsCommand.Execute(null);
}
}
While overriding OnNavigatedTo works fine, there is one more simple technique to run something once your query param is set, given you do not need to run anything asynchronous inside the method: implementing partial method OnFieldAssignedWbsChanged, auto-generated for your convenience by mvvm toolkit
partial void OnFieldAssignedWbsChanged(FieldAssignedWbs value)
{
// run synchronous post query param set actions here
}
Less amount of code and less code-behind and viewModel dependencies, but works fine for non-async operations only.

.Net Maui MVVM - What is the best approach to populating a CollectionView upon a Page/View opening?

I'm new to .Net Maui but have completed James Montemagno's 4 hour Workshop. Included in the Workshop was:
Creating a Page with a CollectionView
Creating a ViewModel
Creating an async method which calls a data service to retrieve data
Configuring the async method as a ICommand
Binding the data model list to the CollectionView
Binding the Command to a Button
Clicking the button works and populates the CollectionView. How would I go about removing the button and performing this action when the page opens? Note I tried modifying the method by removing the "[ICommand]" which did not work. Also, should this action be done in the Code Behind or in the ViewModel?
Thanks in advance for assistance!
(ModelView)
public partial class FieldAssignedWbsViewModel : BaseViewModel
{
FieldAssignedWbsService fieldAssignedWbsService;
public ObservableCollection<FieldAssignedWbs> WbsList { get; set; } = new();
public FieldAssignedWbsViewModel(FieldAssignedWbsService fieldAssignedWbsService)
{
Title = "Wbs Assigned";
this.fieldAssignedWbsService = fieldAssignedWbsService;
}
[ICommand]
async Task GetFieldAssignedWbsListAsync()
{
if (IsBusy)
return;
try
{
IsBusy = true;
var wbsList = await fieldAssignedWbsService.GetFieldAssignedWbsList();
if (WbsList.Count != 0)
WbsList.Clear();
foreach (var wbs in wbsList)
WbsList.Add(wbs);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
await Shell.Current.DisplayAlert("Error!",
$"Undable to get monkeys: {ex.Message}", "OK");
}
finally
{
IsBusy = false;
}
}
}
(CollectionView Binding)
<CollectionView BackgroundColor="Transparent"
ItemsSource="{Binding WbsList}"
SelectionMode="None">
(Code behind page with incorrect call to Command Method)
public partial class FieldAssignedWbsPage : ContentPage
{
public FieldAssignedWbsPage(FieldAssignedWbsViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
//The following call does not work
//Hover message: Non-invocable member... cannot be called like a method
await viewModel.GetFieldAssignedWbsListCommand();
}
}
Although the original answer is very valid, I'd recommend installing the CommunityToolkit.Maui (by Microsoft), then using its EventToCommand features.
After installing, add builder.UseMauiCommunityToolkit() to CreateMauiApp() method in MauiProgram.cs.
Then, in relevant XAML page, add this namespace xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit" and then you should be able to use this block of code to do what you want:
<ContentPage.Behaviors>
<toolkit:EventToCommandBehavior
EventName="Appearing"
Command="{Binding GetFieldAssignedWbsListCommand}" />
</ContentPage.Behaviors>
Sorry that this is a bit late, I just believe that it is a slightly cleaner solution as it avoids populating the code-behind with any code and keeps UI handling purely between the viewmodel and the view.
use OnAppearing. You may also need to make the GetFieldAssignedWbsList public
protected override async void OnAppearing()
{
await viewModel.GetFieldAssignedWbsListCommand.Execute(null);
}

How to access code behind from Tablet/Phone template?

I am taking over a project with a DashboardPage and a DashboardPageViewModel that are linked through DI's ViewModelLocator. The Dashboard page have the following code that separates the Xaml/Code behinds into two separate templates.
private void SetContent()
{
Debug.WriteLine("Dashboardpage setContent");
switch(Device.Idiom)
{
case TargetIdiom.Phone:
Content = new PrimaryPhoneLayout
{
RegionContent = RegionContent
};
break;
case TargetIdiom.Tablet:
Content = new PrimaryTabletLayout(deviceDisplay)
{
RegionContent = RegionContent
};
break;
default:
throw new NotSupportedException($"{Device.Idiom} is not a supported idom");
}
}
I want to add a button in both Phone/Tablet xaml and handle the logic within DashboardPage. How can I make a reference to Dashboard page when these XAML files are linked to their individual code behind and not Dashboard Page?
To elaborate further, DashboardPage derives from MenuContainerPage that allows me to slide in/out of my slide menu. I want to handle this logic through a button that I implemented in both Tablet/Phone layout.
This is how I would do it.
I would start by creating an interface with the events I want to expose from my ContentView
For the sample, I will call this interface as IMenuOptionHandler and it would look like this
public interface IMenuOptionHandler
{
event EventHandler OnSlideIn;
event EventHandler OnSlideOut;
}
Here we have two events that will be invoked from our ContentViews. You can add as many as you wish.
Then we need to make our ContentViews to implement this interface:
public partial class PrimaryPhoneLayouts : ContentView, IMenuOptionHandler
{
//...
#region "IMenuOptionHandler implementation"
public event EventHandler OnSlideIn;
public event EventHandler OnSlideOut;
#endregion
void OnSlideInButtonClicked(object sender, EventArgs e)
{
OnSlideIn?.Invoke(this, EventArgs.Empty);
}
void OnSlideOutButtonClicked(object sender, EventArgs e)
{
OnSlideOut?.Invoke(this, EventArgs.Empty);
}
}
public partial class PrimaryTabletLayout : ContentView, IMenuOptionHandler
{
// ...
#region "IMenuOptionHandler implementation"
public event EventHandler OnSlideIn;
public event EventHandler OnSlideOut;
#endregion
void OnSlideInButtonClicked(object sender, EventArgs e)
{
OnSlideIn?.Invoke(this, EventArgs.Empty);
}
void OnSlideOutButtonClicked(object sender, EventArgs e)
{
OnSlideOut?.Invoke(this, EventArgs.Empty);
}
As you can see both classes are implementing our interface.
Also, I added two sets of methods which are the methods that you will hook to the Buttons on the XAML.
Let's imagine that your XAML looks like this:
<ContentView.Content>
<StackLayout Orientation="Vertical"
HorizontalOptions="FillAndExpand">
<Button Text="SlideIn"
VerticalOptions="CenterAndExpand"
HorizontalOptions="FillAndExpand"
Clicked="OnSlideInButtonClicked" />
<Button Text="SlideOut"
VerticalOptions="CenterAndExpand"
HorizontalOptions="FillAndExpand"
Clicked="OnSlideOutButtonClicked" />
</StackLayout>
</ContentView.Content>
Both XAML should have the buttons and the Clicked events wired to our methods in the Code behind classes.
These two methods the only purpose (as of now) is to invoke the events and notify anyone that it's subscribed to them that an event happened.
Now in your DashboardPage
you will add this global property for simplicity
IMenuOptionHandler MenuOptionHandler => Content as IMenuOptionHandler;
This will cast the Content of the Page, whatever it's the value, to IMenuOptionHandler. Any class that implements this interface will allow this cast to happen.
The last part to add on the same DashboardPage is the subscription to the events. These are gonna happen in the OnAppearing method and we will be unsubscribing on the OnDisappearing.
protected override void OnAppearing()
{
base.OnAppearing();
if (MenuOptionHandler != null)
{
MenuOptionHandler.OnSlideIn += MenuOptionHandler_OnSlideIn;
MenuOptionHandler.OnSlideOut += MenuOptionHandler_OnSlideOut;
}
}
protected override void OnDisappearing()
{
base.OnDisappearing();
if (MenuOptionHandler != null)
{
MenuOptionHandler.OnSlideIn -= MenuOptionHandler_OnSlideIn;
MenuOptionHandler.OnSlideOut -= MenuOptionHandler_OnSlideOut;
}
}
void MenuOptionHandler_OnSlideIn(object sender, EventArgs e)
{
//Logic to handle the SlideIn
Debug.WriteLine("MenuOptionHandler_OnSlideIn");
}
void MenuOptionHandler_OnSlideOut(object sender, EventArgs e)
{
//Logic to handle the SlideOut
Debug.WriteLine("MenuOptionHandler_OnSlideOut");
}
Now, whenever one of the Buttons on the ContentView (iPhone or Tablet) is clicked, the Dashboard ContentPage will be notified about this and you will be able to perform any task you wish.
Hope this helps.-
Assume you have a button in Page1, first give a name to the Button in Xaml:
<Button x:Name="btnInPage1" Text="Welcome to Xamarin.Forms!" />
In the code behind of Page1, create a public static property of button, and set the btnPageOne = btnInPage1:
public partial class Page1 : ContentPage
{
public static Button btnPageOne;
public Page1 ()
{
InitializeComponent ();
btnPageOne = btnInPage1;
}
}
Then in your DashboardPage, you can access the button by using Page1.btnPageOne, and handle the logic with:
Page1.btnPageOne.Clicked += delegate {
Console.WriteLine("Page1 btn clicked");
};
The same if you have Page2, Page3...

Using Rg Plugins Popup with Xamarin Forms

I am very new to Xamarin Forms development and I need a popup dialog. I found exactly what I am looking for in https://github.com/rotorgames/Rg.Plugins.Popup, but I cannot for the life of me figure out how to use it. Could someone point me to a working example or provide some direction on use? The README.md on the site is not helping me much.
I want the the popup dialog to appear when a info button is clicked in the top navigation bar. All the popup needs is 1-2 buttons (and labels) for setting user settings.
This is for Xamarin.Forms: iOS and Android.
In simple steps:
Install the plugin in all the projects
Add the PopUp in your
Xaml
Use the methods they provide on the documentacion for Show/Hide the PopUp:
Task PushAsync(PopupPage page, bool animate = true)
Task PopAllAsync(bool animate = true)
They also provide a demo, check it:
https://github.com/rotorgames/Rg.Plugins.Popup/tree/master/src/Demo
Add a reference to the library, i.e. from nuget, to all projects.
Within your Android project, add this Rg.Plugins.Popup.Popup.Init(this, savedInstanceState); inside the MainActivity.cs OnCreate method, before Xamarin Forms Inits.
And the same for the iOS project, inside AppDelegate.cs FinishedLaunching method()
//Android
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Rg.Plugins.Popup.Popup.Init(this, savedInstanceState); /*Must add before the other Xamarin Inits*/
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
Xamarin.Forms.Forms.Init(this, savedInstanceState);
}
//iOS
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
Rg.Plugins.Popup.Popup.Init(); /* place at the top*/
....
}
Add a new ContentPage (.xaml) to your Views directory.
<?xml version="1.0" encoding="utf-8" ?>
<pages:PopupPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:Rg.Plugins.Popup.Pages;assembly=Rg.Plugins.Popup"
xmlns:animations="clr-namespace:Rg.Plugins.Popup.Animations; assembly=Rg.Plugins.Popup"
x:Class="MyProjectName.Views.MyContentPageName">
<pages:PopupPage.Animation>
<animations:ScaleAnimation
PositionIn="Center"
PositionOut="Center"
ScaleIn="1.2"
ScaleOut="0.8"
DurationIn="400"
DurationOut="300"
EasingIn="SinOut"
EasingOut="SinIn"
HasBackgroundAnimation="True"/>
</pages:PopupPage.Animation>
<StackLayout HorizontalAlignment="FillAndExpand" VerticalAlignment="FillAndExpand">
<!-- place your layout content here ....fx a close popup button -->
<Button Clicked="CloseBtn_Clicked" Text="Close" />
</StackLayout>
</pages:PopupPage>
In the ContentPage (PopupPage) code behind file, add using Rg.Plugins.Popup.Services; and inherit from the following
using Rg.Plugins.Popup.Services;
using System;
using System.Threading.Tasks;
using Xamarin.Forms;
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MyContentPageName: Rg.Plugins.Popup.Pages.PopupPage
{
public MyContentPageName()
{
InitializeComponent();
}
public void OnAnimationStarted(bool isPopAnimation)
{
// optional code here
}
public void OnAnimationFinished(bool isPopAnimation)
{
// optional code here
}
protected override bool OnBackButtonPressed()
{
// Return true if you don't want to close this popup page when a back button is pressed
return true;
}
// Invoked when background is clicked
protected override bool OnBackgroundClicked()
{
// Return false if you don't want to close this popup page when a background of the popup page is clicked
return false;
}
private async void CloseBtn_Clicked(object sender, EventArgs e)
{
await PopupNavigation.Instance.PopAsync(true);
}
}
From the .xaml.cs page, where you would like to open the popup, add this:
using System;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using Rg.Plugins.Popup.Contracts;
using Rg.Plugins.Popup.Services;
public partial class MyOtherPage : ContentPage
{
private IPopupNavigation _popup { get; set; }
private MyContentPageName _modalPage;
public MyOtherPage()
{
_popup = PopupNavigation.Instance;
_modalPage = new MyContentPageName();
}
protected override void OnAppearing()
{
base.OnAppearing();
_popup.Popped += Popup_Popped;
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_popup.Popped -= Popup_Popped;
}
private async void Tapped_OpenModal(object sender, EventArgs e)
{
await _popup.PushAsync(_modalPage);
}
/// <summary> Triggered when the MyContentPageName popup is closed "PopAsync()" </summary>
private async void Popup_Popped(object sender, Rg.Plugins.Popup.Events.PopupNavigationEventArgs e)
{
/* add your logic here, if necessary */
}
}
*Note: If your modal simply displays static content, there is no need for a _popped event delegate within the OnAppearing()/OnDisappearing().

Can't get access to controls from code behind in pcl part of xamarin forms project

I've got Xamarin.Forms project with pcl-part and native win, ios and android parts.
All page structure and view-models are in pcl-part. App work's fine, but when I'm trying for example to hide Grid from code behind - it do nothing. Here is code example:
Xaml:
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="SomeNamespase.SomePage">
<Grid x:Name="InnerGrid" BackgroundColor="Green">
<Frame x:Name="InnerContent"/>
</Grid>
</ContentPage>
.cs :
using System;
namespace SomeNamespase
{
public partial class SomePage : ContentPage
{
public void SomeMethod()
{
this.InnerGrid.IsVisible = false;
this.InnerContent.BackgroundColor = Color.Aqua;
}
}
}
I've also tried this.FindByName<Grid>("InnerGrid"); the same result
Note: if I am trying to get controls from action in PCL everything is good. Nothing going on when I'm trying to get controls from ViewPresenter in windows (or other platforms) project.
You need to make sure you are properly implementing INotifyPropertyChanged
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Please try the below code, as in your code I can't see the constructor.
using System;
namespace SomeNamespase
{
public partial class SomePage : ContentPage
{
public SomePage()
{
SomeMethod() ;
}
public void SomeMethod()
{
this.InnerGrid.IsVisible = false;
this.InnerContent.BackgroundColor = Color.Aqua;
}
}
}