Xamarin forms TabbedPage View Model called multiple time - mvvm

I have implemented Tabbedpage using ViewModel but my ViewModel constructor call 4 times because I create 4 tabs, I also used prism for ViewModel binding.
Below is a design file
<?xml version="1.0" encoding="UTF-8"?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
xmlns:material="clr-namespace:XF.Material.Forms.UI;assembly=XF.Material"
xmlns:ffimageloading="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
xmlns:ffTransformations="clr-namespace:FFImageLoading.Transformations;assembly=FFImageLoading.Transformations"
prism:ViewModelLocator.AutowireViewModel="True"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
xmlns:extended="clr-namespace:Xamarin.Forms.Extended;assembly=Xamarin.Forms.Extended.InfiniteScrolling"
xmlns:customcontrols="clr-namespace:QuranicQuizzes.CustomControls"
xmlns:local="clr-namespace:QuranicQuizzes.Views" NavigationPage.HasNavigationBar="True"
x:Class="QuranicQuizzes.Views.DashboardPage">
<NavigationPage.TitleView>
<StackLayout Orientation="Horizontal" HorizontalOptions="FillAndExpand">
<Label Text="Dashboard" TextColor="White" HorizontalTextAlignment="Center" HorizontalOptions="CenterAndExpand" VerticalTextAlignment="Center" FontFamily="{StaticResource QuranFontBold}" FontSize="Medium" />
<StackLayout Orientation="Horizontal">
<material:MaterialMenuButton x:Name="Menus" ButtonType="Text" Image="list" TintColor="White" BackgroundColor="Transparent" CornerRadius="24" Choices="{Binding Actions}" MenuSelected="MaterialMenuButton_MenuSelected" />
</StackLayout>
</StackLayout>
</NavigationPage.TitleView>
<local:HomeTabPage/>
<local:QuizzesTabPage/>
<local:LiveGameTabPage/>
<local:AssignmentTabPage/>
</TabbedPage>
Below is my code
public partial class DashboardPage : TabbedPage
{
private DashboardPageViewModel vm;
public DashboardPage()
{
try
{
InitializeComponent();
vm = BindingContext as DashboardPageViewModel;
}
catch (Exception ex)
{
}
}
}
Below is my ViewModel
public class DashboardPageViewModel : ViewModelBase
{
INavigationService _navigationService;
IClientAPI _clientAPI;
Dashboards dashboard;
public DashboardPageViewModel(INavigationService navigationService, IClientAPI clientAPI) : base(navigationService)
{
_navigationService = navigationService;
_clientAPI = clientAPI;
if (CrossConnectivity.Current.IsConnected)
{
var StartDate = DateTime.Now.AddDays(-7).ToString("yyyy-MM-dd");
var Enddate = DateTime.Now.ToString("yyyy-MM-dd");
if (dashboard == null)
{
dashboard = new Dashboards();
getDashboardData(StartDate, Enddate);
}
}
}
}

I see what you're trying to do. You want to initialise your vm instance so that you can access you vm from your view.
Instead of doing this:
vm = BindingContext as DashboardPageViewModel;
what we can do is change the type of the existing BindingContext property by doing this:
public partial class DashboardPage
{
new DashboardPageViewModel BindingContext
{
get => (DashboardPageViewModel) base.BindingContext;
set => base.BindingContext = value;
}
public DashboardPage()
{
InitializeComponent();
}
}
now you can just access BindingContext.DoSomething because its type is now DashboardPageViewModel.
Now that's sorted out, your viewmodel should not be being called 4 times! Something is wrong here. Here is a checklist of things to do that may be causing the constructor being called 4 times as not a lot more info was provided.
Try removing <NavigationPage.TitleView> segment.
Make sure you are navigating to DashboardPage.
Make sure that each individual TabbedPage has it's own viewmodel.
Try removing prism:ViewModelLocator.AutowireViewModel="True"and manually adding the viewmodel to the TabbedPage.
Finally constructors should be able to run very fast and should only be used for assigning variables or instantiation or very quick operations. What you could maybe do is separate the code in your VM:
public class DashboardPageViewModel : ViewModelBase
{
IClientAPI _clientAPI;
Dashboards dashboard;
public DashboardPageViewModel(INavigationService navigationService, IClientAPI clientAPI) : base(navigationService)
{
_clientAPI = clientAPI;
}
public void Init()
{
if (CrossConnectivity.Current.IsConnected)
{
var StartDate = DateTime.Now.AddDays(-7).ToString("yyyy-MM-dd");
var Enddate = DateTime.Now.ToString("yyyy-MM-dd");
if (dashboard == null)
{
dashboard = new Dashboards();
getDashboardData(StartDate, Enddate);
}
}
}
}
and then in your view you could add this method:
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
if(BindingContext == null)
{
return;
}
BindingContext.Init();
}
I hope this really helps you.
NB: All this code was written on the fly and never compiled, there may be some errors.

Related

How to implement Activity/Wait Indicator in dotnet Maui?

I needed to implement a wait indicator for a page in my Maui app.
Searching gave me this, but no step by step instructions.
So how do I do this?
Overview:
The control to display the animation is called ActivityIndicator.
ActivityIndicator is a visual element, should be part of your page.
So, add an ActivityIndicator to your xaml.
The state of the indicator is part of logic - should live in your view model.
So, add a bindable property to your view model, and bind ActivityIndicator.IsRunning to this property.
Sample (I haven't tested, just for illustration)
Page (xaml):
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:customcontrols="clr-namespace:Waiter.Maui.CustomControls"
x:Class="..." >
<ContentPage.Content>
<ActivityIndicator IsRunning="{Binding IsBusy}" />
<Button Text="Go" Command="{Binding GoCommand}" />
</ContentPage.Content>
</ContentPage>
ViewModel:
namespace MyNamespace
{
public class MyViewModel : BaseViewModel
{
public MyViewModel()
{
GoCommand = new Command(execute: OnGo, canExecute: true);
}
public Command GoCommand { get; }
private void OnGo()
{
MainThread.InvokeOnMainThreadAsync(async () =>
{
IsBusy = true;
Thread.Sleep(5000);
IsBusy = false;
return result;
});
}
}
}
BaseViewModel class (so that it can be re-used, from existing community content):
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Waiter.Maui.Pages
{
public class BaseViewModel : INotifyPropertyChanged
{
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 want to point out few things.
First of all, this "IsBusy" that I see getting recommended all around, is working strategy. I can only recommend using CommunityToolkit.MVVM, and letting it do your job, and handle all notification code instead of you.
However, using such boolean variable, is no different than using Lock, Mutex, Semaphore, etc. A programmer has to be very careful how and when it is changed, otherwise all kinds of bugs may occur.
In reality, most problems can be solved with commanding itself.
Specifically CanExecute property is more than enough.
I recommend this:
https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/data-binding/commanding?view=net-maui-7.0
Before becoming slave to manual changing bool variables.

Custom control - event handler to main app when inside value changed

I want to be able to add an event from my custom control to the outside world
as I change a value in my custom control, I use TwoWay, but I also want an event to be generated in my main app
I just cannot find anything relevant to this
the main app xaml contains this line :
<custom:MyComp x:Name="myComp" ValueChanged="SomeFunction" Value="{ Binding Path=someIntVariable, Mode=TwoWay}"></custom:MyComp>
how do I implement a ValueChanged event in the custom control ?
my control xaml:
<UserControl ...>
<StackPanel Orientation="Horizontal" >
...
<TextBox x:Name="MyCompTextBox" Width="50" Height="20" TextChanged="MyCompTextBox_TextChanged"/>
...
</StackPanel>
</UserControl>
the code
public partial class MyComp: UserControl
{
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(int), typeof(MyComp), new FrameworkPropertyMetadata(0, OnValuePropertyChangedCallback));
public int Value
{
get { return (int)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
private static void OnValuePropertyChangedCallback(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
MyComp mMyComp= source as MyComp;
int newValue=(int)e.NewValue;
myComp.MyCompTextBox.Text = newValue.ToString();
}
public MyComp()
{
InitializeComponent();
}
private void MyCompTextBox_TextChanged(object sender, EventArgs e)
{
int _bpm = int.ParseMyCompTextBox.Text);
Value = _bpm;
MyCompTextBox.Text = Value.ToString();
// here I want to trigger an even to the main app !!!
}
}
thanks for your help

CommunityToolkit.Maui.Core.Views.MauiPopup throws System.ObjectDisposedException on Close()

The following code results in a System.ObjectDisposedException on the line Close(result); in BarcodeScannerPopup.xaml.cs when a barcode is detected by the CameraBarcodeReaderView.
The detected barcode is correctly displayed in the label in BarcodePage.
Any idea why this exception is thrown?
BarcodePage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
x:Class="MyApp.BarcodePage"
x:DataType="vm:BarcodeViewModel"
xmlns:vm="clr-namespace:MyApp.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui">
<VerticalStackLayout
Margin="20">
<Label
Text="{Binding Barcode}"/>
<Button
Command="{Binding ScanBarcodeClickCommand}"
Text="Scan barcode"/>
</VerticalStackLayout>
</ContentPage>
BarcodePage.xaml.cs
using MyApp.ViewModels;
namespace MyApp.Views;
public partial class BarcodePage : ContentPage
{
public BarcodePage(BarcodeViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}
BarcodeScannerPopup.xaml
<?xml version="1.0" encoding="utf-8" ?>
<toolkit:Popup
x:Class="MyApp.BarcodeScannerPopup"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:zxing="clr-namespace:ZXing.Net.Maui.Controls;assembly=ZXing.Net.MAUI"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui">
<VerticalStackLayout
Margin="20">
<Label
Text="Add a barcode by scanning it."
VerticalOptions="Center"
HorizontalOptions="Center"/>
<zxing:CameraBarcodeReaderView
x:Name="barcodeReader"
BarcodesDetected="BarcodesDetected"
HeightRequest="300"
IsDetecting="True"
Margin="5"
WidthRequest="300"/>
</VerticalStackLayout>
</toolkit:Popup>
BarcodeScannerPopup.xaml.cs
using CommunityToolkit.Maui.Views;
using ZXing.Net.Maui;
namespace MyApp.Views;
public partial class BarcodeScannerPopup : Popup
{
public BarcodeScannerPopup()
{
InitializeComponent();
barcodeReader.Options = new BarcodeReaderOptions
{
AutoRotate = true,
Multiple = false
};
}
private void BarcodesDetected(object sender, BarcodeDetectionEventArgs e)
{
var result = e.Results[0].Value;
Close(result);
}
}
BarcodeViewModel.cs
using CommunityToolkit.Maui.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MyApp.Views;
namespace MyApp.ViewModels;
public partial class BarcodeViewModel : ObservableObject
{
[ObservableProperty]
private string _barcode;
private BarcodeScannerPopup _popup = new BarcodeScannerPopup();
[RelayCommand]
public async Task ScanBarcodeClick()
{
Barcode = (string)await Application.Current.MainPage.ShowPopupAsync(_popup);
}
}
#ToolmakerSteve was right. The CameraBarcodeReaderView continues sending data which results in the System.ObjectDisposedException. setting the IsDetecting property to false prevents this. As this was a PoC I didn't add a proper null check on e, so I've added as a good coding practise in this example.
BarcodeScannerPopup.xaml.cs
using CommunityToolkit.Maui.Views;
using ZXing.Net.Maui;
namespace MyApp.Views;
public partial class BarcodeScannerPopup : Popup
{
public BarcodeScannerPopup()
{
InitializeComponent();
barcodeReader.Options = new BarcodeReaderOptions
{
AutoRotate = true,
Multiple = false
};
}
private void BarcodesDetected(object sender, BarcodeDetectionEventArgs e)
{
var result = e?.Results?.Any() == true
? e.Results[0].Value
: string.Empty;
IsDetecting = false;
Close(result);
}
}

How to call TapGestureRecognizer from ViewModel

I'm trying to implement TapGestureRecognizer which will be called in ViewModel (xaml.cs) not in View class...
Here is sample code in xaml file: (IrrigNetPage.xaml)
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:i18n="clr-namespace:agroNet.AppResource;assembly=agroNet"
xmlns:viewModels="clr-namespace:agroNet.ViewModel"
x:Class="agroNet.View.IrrigNetPage"
BackgroundColor="#EBEBEB">
<Grid>
<Grid.GestureRecognizers>
<TapGestureRecognizer Tapped="HideListOnTap"/>
</Grid.GestureRecognizers>
</Grid>
I implemented HideListOnTap in xaml.cs page (view) like this: (IrrigNetPage.xaml.cs)
int visibility = 1;
private void HideListOnTap(object sender, EventArgs e)
{
visibility++;
if ((visibility % 2) == 0)
{
IrrigList.IsVisible = false;
}
else
{
IrrigList.IsVisible = true;
}
}
It's working fine, but how to do the same thing usin ViewModel?
(How to bind Gesture recognizer from (IrrigNetPage.xaml) with HideListOnTap in IrrigNetViewModel )
Use a Command whenever you want to handle some event in the ViewModel. Without passing any arguments the code would look like as follows
<!-- in IrrigNetPage.xaml -->
<TapGestureRecognizer Command="{Binding HideListOnTapCommand}"/>
And in the ViewModel IrrigNetPageViewModel.cs
public ICommand HideListOnTapCommand { get; }
public IrrigNetPageViewModel()
{
HideListOnTapCommand = new Command(HideListOnTap);
// if HideListOnTap is async create your command like this
// HideListOnTapCommand = new Command(async() => await HideListOnTap());
}
private void HideListOnTap()
{
// do something
}

Best practice for nesting ContentViews into ContentPage as Xamarin architecture

I am have an app that basically loads a bunch of ContentViews into the home ContentPage. I THINK there is a problem here because all of the viewmodels would essentially need to been initialized every time we load the home page. I am wondering if it is worth my time to ditch the below code and convert the views from ContentView's to ContentPage's and just do Navigation.PushAsync(new View1()); instead. Sorry I know this is alot of example code but I would really like to get a clear picture of best practice.
My Home.xaml
<Grid x:Name="ContentBody" VerticalOptions="FillAndExpand">
<local:View1 Grid.Row="0" x:Name="View1" IsVisible="{Binding View1IsVisible}" BindingContext="{Binding View1ViewModel}" />
<local:View2 Grid.Row="0" x:Name="View2" IsVisible="{Binding View2IsVisible}" BindingContext="{Binding View2ViewModel}" />
<local:View3 Grid.Row="0" x:Name="View3" IsVisible="{Binding View3IsVisible}" BindingContext="{Binding View3ViewModel}" />
<local:View4 Grid.Row="0" x:Name="View4" IsVisible="{Binding View4IsVisible}" BindingContext="{Binding View4ViewModel}" />
<local:View5 Grid.Row="0" x:Name="View5" IsVisible="{Binding View5IsVisible}" BindingContext="{Binding View5ViewModel}" />
<local:View6 Grid.Row="0" x:Name="View6" IsVisible="{Binding View6IsVisible}" BindingContext="{Binding View6ViewModel}" />
<local:DrawerView Grid.Row="0" x:Name="DrawerView" IsVisible="{Binding DrawerViewIsVisible}" />
</Grid>
Then In my HomeViewModel...
private readonly View1ViewModel _view1ViewModel = new View1ViewModel();
public View1ViewModel View1ViewModel { get { return _view1ViewModel; } }
private readonly View2ViewModel _view2ViewModel = new View2ViewModel();
public View2ViewModel View2ViewModel { get { return _view2ViewModel; } }
private readonly View3ViewModel _view3ViewModel = new View3ViewModel();
public View3ViewModel View3ViewModel { get { return _view3ViewModel; } }
private readonly View4ViewModel _view4ViewModel = new View4ViewModel();
public View4ViewModel View4ViewModel { get { return _view4ViewModel; } }
private readonly View5ViewModel _view5ViewModel = new View5ViewModel();
public View5ViewModel View5ViewModel { get { return _view5ViewModel; } }
private readonly View6ViewModel _view6ViewModel = new View6ViewModel();
public View6ViewModel View6ViewModel { get { return _view6ViewModel; } }
///////////////////Some Visibility Properties...//////////////////////
///////////////////Some Visibility Properties...//////////////////////
private bool _view1IsVisible;
public bool View1IsVisible
{
get { return _view1IsVisible; }
set { _view1IsVisible = value; OnPropertyChanged("View1IsVisible"); }
}
private bool _view2IsVisible;
public bool View2IsVisible
{
get { return _view2IsVisible; }
set { _view2IsVisible = value; OnPropertyChanged("View2IsVisible"); }
}
private bool _view3IsVisible;
public bool View3IsVisible
{
get { return _view3IsVisible; }
set { _view3IsVisible = value; OnPropertyChanged("View3IsVisible"); }
}
private bool _view4IsVisible;
public bool View4IsVisible
{
get { return _view4IsVisible; }
set { _view4IsVisible = value; OnPropertyChanged("View4IsVisible"); }
}
private bool _view5IsVisible;
public bool View5IsVisible
{
get { return _view5IsVisible; }
set { _view5IsVisible = value; OnPropertyChanged("View5IsVisible"); }
}
private bool _view6IsVisible;
public bool View6IsVisible
{
get { return _view6IsVisible; }
set { _view6IsVisible = value; OnPropertyChanged("View6IsVisible"); }
}
/////And then this is more or less a method to show the view/////////////
private void ShowView(ViewChangedEventArgs e)
{
HideAllViews();
switch(e.SelectedView){
case ViewType.View1:
View1IsVisible = true
break;
case ViewType.View2:
View2IsVisible = true
break;
case ViewType.View3:
View3IsVisible = true
break;
case ViewType.View4:
View4IsVisible = true
break;
case ViewType.View5:
View5IsVisible = true
break;
case ViewType.View6:
View6IsVisible = true
break;
}
}
Can someone tell me if this approach is fine? as using this approach everytime I add a new page I will need to add a view to the Homepage view and the viewModel & IsVisible properties to the Homepage ViewModel..
I would greatly appreciate any guidance on this. I think a better approach would be to just seperate the ContentViews from the HomePage and when I Navigate to one of these views I would just PushAsync. I have seen supporting documentation online where some people are taking the above approach, I am just trying to ask the experts what they think when they see this code.
After speaking w/ a member of the Xamarin team I've been told to remove all the views from this homeview page, bind the perspective view models in their own code behinds, and from there I can navigation using Navigation.PushAsync(new View1());
Tutorial:
https://github.com/XamarinUniversity/XAM290
Documentaiton:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/enterprise-application-patterns/