How to call TapGestureRecognizer from ViewModel - mvvm

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
}

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

Xamarin forms TabbedPage View Model called multiple time

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.

Bind Plugin.Media fromViewModel

I have a view model that handles the Plugin.Media to take a picture from the phone. The image will not show unless I have the code below (View Model) in the code-behind, then it works fine, which is defeating the object of me learning MVVM. I have also tried 'FileImageSource' and used 'Source' in its various ways.'
Can anyone explain what I am doing wrong?
View Model:
public string FilePath { get => _filepath; set { _filepath = value; OnPropertyChanged(); } }
private string _filepath;
public Command CaptureImage
{
get
{
return new Command(TakePicture);
}
}
//
private async void TakePicture()
{
if (!CrossMedia.Current.IsCameraAvailable || !CrossMedia.Current.IsTakePhotoSupported)
{
//say something
return;
}
var file = await CrossMedia.Current.TakePhotoAsync(new Plugin.Media.Abstractions.StoreCameraMediaOptions
{
Directory = "FoodSnap",
Name = GetTimestamp(DateTime.Now),
PhotoSize = Plugin.Media.Abstractions.PhotoSize.Custom,
CustomPhotoSize = 50
});
if (file == null)
return;
FilePath = file.Path;
}
XAML:
<pages:PopupPage
xmlns:pages="clr-
namespace:Rg.Plugins.Popup.Pages;assembly=Rg.Plugins.Popup"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:TacticalFitness.ViewModels"
x:Class="TacticalFitness.Views.PopUps.AddFoodSnapPopUp">
<BindableObject.BindingContext>
<vm:AddSnapViewModel/>
</BindableObject.BindingContext>
<StackLayout>
<Image Source="{Binding FilePath}" HeightRequest="150" BackgroundColor="LightGray" >
<Image.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding CaptureImage}"
NumberOfTapsRequired="1" />
</Image.GestureRecognizers>
</Image>
</StackLayout>
</pages:PopupPage>
If you want to use this from your view model, assign an instance of the view model to the BindingContext of your page, so that is the only line in your code-behind basically.
public YourPage()
{
InitializeComponent();
BindingContext = new YourViewModel();
}
Now it should work.
Update
From your XAML I see:
<Image.Source>
<StreamImageSource Stream="{Binding NewImage}"/>
</Image.Source>
Where Stream is a type of Stream but you are assigning an ImageSource to it. You can simply do this: <Image HeightRequest="150" BackgroundColor="LightGray" Source="{Binding NewImage}">