xamarin.forms Handle Clicked event on WebView - xamarin.forms

I want to handle the click/tap event on a WebView control
I've tried the GestureRecognizers but nothing happens, i think maybe the WebView has some sort of making the event handled "true".
<WebView>
<WebView.GestureRecognizers>
<TapGestureRecognizer
Tapped="OnWebViewClick"
NumberOfTapsRequired="1" />
</WebView.GestureRecognizers>
</WebView>
And I've tried it using c# code behind too but no luck :/

I am using this hack: put the WebView in a Grid and insert a BoxView in the same cell. Then put a handler on the BoxView.

You can use the HybridWebView from XLabs, and use javascript injection to invoke and handle clicks in your Xamarin control. The injected javascript code can add a click-event listener at capture stage. When a click is detected it uses Native callback to send information back to C# code.
For example - you can define a custom control like this:
public class ClickEventArgs : EventArgs
{
public string Element { get; set; }
}
public class ClickableWebView : XLabs.Forms.Controls.HybridWebView
{
public event EventHandler<ClickEventArgs> Clicked;
public static readonly BindableProperty ClickCommandProperty =
BindableProperty.Create("ClickCommand", typeof(ICommand), typeof(ClickableWebView), null);
public ICommand ClickCommand
{
get { return (ICommand)GetValue(ClickCommandProperty); }
set { SetValue(ClickCommandProperty, value); }
}
public ClickableWebView()
{
LoadFinished += (sender, e) =>
InjectJavaScript(#"
document.body.addEventListener('click', function(e) {
e = e || window.event;
var target = e.target || e.srcElement;
Native('invokeClick', 'tag='+target.tagName+' id='+target.id+' name='+target.name);
}, true /* to ensure we capture it first*/);
");
this.RegisterCallback("invokeClick", (string el) => {
var args = new ClickEventArgs { Element = el };
Clicked?.Invoke(this, args);
ClickCommand?.Execute(args);
});
}
}
Sample XAML usage
<local:ClickableWebView
Uri="https://google.com"
Clicked="Handle_Clicked"
/>
and sample code-behind
void Handle_Clicked(object sender, CustomWebView.ClickEventArgs e)
{
Xamarin.Forms.Application.Current.MainPage.DisplayAlert("WebView Clicked", e.Element, "Dismiss");
}
** Output **
Alternatively, you can also bind ClickCommand property to implement this using MVVM pattern.

Another option is to handle the click in html and do a navigation that doesn't go anywhere. You can put something like this in your html
<div onclick="window.location.href='#click#'">...</div>
So a click anywhere inside there would cause a navigation. If you only have a button, you could just use
<a href='#click'>...</a>
Then in your WebView control wire up the Navigating event, and check if the new url contains "#click". If so, do your click handling code and call e.Cancel=true in the event to prevent the browser from completing the navigation.
Note that onclick handlers don't work on body or document elements in Xamarin Webview. At least not on iOS.

I've found the simplest approach is to use a Grid with two controls, the WebView and a Button
<Grid>
<WebView
Grid.Column="0"
Grid.Row="0"
HeightRequest="100"
WidthRequest="1000" />
<Button
Grid.Column="0"
Grid.Row="0"
BackgroundColor="Transparent"
HorizontalOptions="Fill"
VerticalOptions="Fill"
Clicked="OnWebViewTapped"/>
</Grid>
The button covers the WebView and intercepts taps.

Gesture recognizer doesn't work with WebView. You can try using MR.Gestures
To get all the features you will have to purchase a license.
If you forget to configure the license key properly or the key does not match your app name, then all the events will still be raised, but the properties of the EventArgs will be empty. That may be enough for the tap and long press events, but not for the more complicated ones.

An easier workaround is to use the 'Focused' event on your webview. You can implement it as below:
var wb = new WebView
{
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand,
Source = "https://stackoverflow.com/questions/56320611/webview-gesturerecognition-not-working-in-xamarin-forms",
};
wb.Focused += (s, e) =>
{
//Handle your logic here!
wb.Unfocus();
};

Related

ImageButton not firing Command binding in Xamarin Forms

I have an ImageButton that is not firing Command binding command using VMMV architecture. First all other bindings are working properly in the view.
Here is button:
<ImageButton Command="{Binding SelectedItemCommand}" Source="{Binding Logo}" Grid.Row="0" Grid.Column="1" HeightRequest="150" WidthRequest="150" HorizontalOptions="CenterAndExpand" VerticalOptions="EndAndExpand"></ImageButton>
and in ViewModel:
public ICommand SelectedItemCommand => new Command(GetSelectedItem);
When I click the image nothing happens. I've even tried to bind to Pressed parameter but from everything I have read only the Command parameter should be used in a binding scenario. Putting a breakpoint on the function GetSelectedItem never gets reached.
What am I doing wrong here?
Sorry been away for a few days. So nothing was working on the suggestions even though they really should be clicking wouldn't fire command. Anyway I managed to get it to fire now using an eventhandler like so:
SelectedItemCommand = new Command<string>(param => OnItemSelected(param));
public void OnItemSelected(string img1_2)
{
PressedEventHandler?.Invoke(this, EventArgs.Empty);
}
The param captures the CommandParameter so I know which image to the question was clicked "img1" "img2" to do something specific. So my function now accepts a sender object and empty eventarg. I would like to instead pass img1_2 value but that doesn't appear to be possible as of now. What is cusrious is the sender object contains all the properties and values from the images (like an array of all my properties) but I cannot seem to get at them.
Attempted this:
string str = Item1Image.ToString(); // property in sender and viewmodel
But this returns a null value and not value listed in the sender object value?
Any additional thoughts?
TIA!
Rick...
public ICommand SelectedItemCommand {get; private set;}
...
public YourViewModel(){
...
SelectedItemCommand = new Command(GetSelectedItem);
...
}
...
Or
public ICommand SelectedItemCommand{
get
{
return new Command(() => {
//Do something
});
}
}

Handling Keyboard Input at the Page Level with Xamarin Forms

I am working on a Xamarin Forms project for which one requirement is to recognize certain key presses to trigger hot key actions. The devices that we will be deploying the application to have physical keyboards attached. For now, Android is the only platform that is being targeted.
From some research that I did yesterday afternoon, it sounds as though a custom page renderer is what is required. As I played with this concept this morning, I stumbled upon the On* key methods of the Activity class.
I tried adding the following to the MainActivity class in the Android project:
public override bool OnKeyUp([GeneratedEnum] Keycode keyCode, KeyEvent e)
{
return base.OnKeyUp(keyCode, e);
}
Placing a breakpoint on this method seems to show that this code is what is needed (read, this method is fired whenever I press a key on the keyboard).
The issue is that this method is also fired when an Entry control on the page has focus. Shouldn't the key press be handled by the Entry control and not bubbled up to the page?
Generally speaking, is this the right approach for what I am trying to accomplish? Are there other approaches that someone can point me to that might work better?
When I was working with hardware devices, I had to do something similar. I created a custom renderer for an entry on the Xamarin.android side. This captures the Enter key press for both hard and soft key in different events. I think creating custom render for a page like you did could work too but this only captures key presses for elements that are in focus. This works for me as I have the entry in focus when user presses the hardware Enter key.
[assembly: ExportRenderer(typeof(CustomEntry), typeof(CustomEntryRenderer))]
namespace Project.Droid.Controls {
public class CustomEntryRenderer : EntryRenderer {
public CustomEntryRenderer(Context context) : base(context) {
}
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e) {
base.OnElementChanged(e);
Control.EditorAction += Control_EditorAction;
Control.KeyPress += NativeEditText_KeyPress;
}
// Fires only for Soft Keyboard
private void Control_EditorAction(object sender, Android.Widget.TextView.EditorActionEventArgs e) {
if (e.ActionId == ImeAction.Done) {
// your code
e.Handled = true;
}
}
// Fires for Hard Keyboard
private void NativeEditText_KeyPress(object sender, KeyEventArgs e) {
if (e.KeyCode == Keycode.Enter && e.Event.Action == KeyEventActions.Up) {
// your code
e.Handled = true;
}
else
e.Handled = false;
}
}
}
FYI: I also tried the MainActivity event that you are using and it did not work for me. I cannot recall why.

What's the correct way to implement login page in Xamarin Shell?

My app has structure like this.
Splash page => Login page => Main page
After login, user cannot go back to login/splash page. There are several pages in flyout menu that user can go to. However, login/splash items should not be showed in these flyout menu items.
Some project may try to load main page first before show login page as a modal page. I think this way doesn't work because it should take so much time to load complex main page before send user back to login.
Main goal of having logins screen is to only show when user is not logged to app.
Answers cover only parts of solution. Here is how i archived my login solution.
My goal: when app starts, then check if user is logged. If so then goto MainPage, else go to LoginPage. Same principle could be used to show app first run screens.
Download ShellLoginSample
Replace order of items in AppShell.xaml
<!-- Your Pages -->
<TabBar Route="main">
<Tab Title="Browse" Icon="tab_feed.png">
<ShellContent ContentTemplate="{DataTemplate local:ItemsPage}" />
</Tab>
<Tab Title="About" Icon="tab_about.png">
<ShellContent ContentTemplate="{DataTemplate local:AboutPage}" />
</Tab>
</TabBar>
<ShellItem Route="login">
<ShellContent ContentTemplate="{DataTemplate local:LoginPage}" />
</ShellItem>
Then edit App.xaml.cs to check login state
public App()
{
InitializeComponent();
DependencyService.Register<MockDataStore>();
var isLoogged = Xamarin.Essentials.SecureStorage.GetAsync("isLogged").Result;
if (isLoogged == "1")
{
MainPage = new AppShell();
}
else
{
MainPage = new LoginPage();
}
}
Then edit in LoginPage.xaml.cs
protected async void OnClicked(object source, EventArgs args)
{
await Xamarin.Essentials.SecureStorage.SetAsync("isLogged", "1");
Application.Current.MainPage = new AppShell();
await Shell.Current.GoToAsync("//main");
}
The best way is to make your LoginPage the landing Page with this code
MainPage = new NavigationPage(new LoginPage());
then after successful login, you then Navigate to ShellApp using the following code
Application.Current.MainPage = new AppShell();
Then you can use Xam.SettingsPlugin to store your token and userId.
To log out simple navigate away from the AppShell by using the following code..
Application.Current.MainPage = new NavigationPage(new LoginPage());
there is a enhanceent request from Xamarin forms github repo requesting inbuilt login page, here is the link link
Xamrin.Forms Vesion:4.6 have made the Login flow very easy. This is the link to official document. The developer have the flexibility to choose the CurrentItem of the Shell irrespective of the order of the items defined in the AppShell.xaml file. This can be done in AppShell.xaml as well as AppShell.xaml.cs
Check Login state in App.xaml.cs and send parameter to AppShell file accordingly
public App()
{
InitializeComponent();
if (isLogged)
{
MainPage = new AppShell("mainpage");
}
else
{
MainPage = new AppShell("loginpage");
}
}
In AppShell.Xaml define your app structure.
<ShellItem>
<ShellContent x:Name="loginpage"
ContentTemplate="{DataTemplate views:LoginPage}"
Route="loginpage" />
</ShellItem>
<TabBar x:Name="mainpage" Route="mainpage">
<ShellContent Title="Dashboard" ContentTemplate="{DataTemplate views:DashboardPage}" />
<ShellContent Title="Finance" ContentTemplate="{DataTemplate views:FinancePage}" />
<ShellContent Title="Profile" ContentTemplate="{DataTemplate views:ProfilePage}" />
</TabBar>
In the AppShell.xaml.cs select the CurrentItem as per the parameter received from App.xaml.cs.
public AppShell(string currentItem)
{
InitializeComponent();
Shell shell = new Shell();
switch (currentItem)
{
case "login":
shell.CurrentItem = loginpage;
break;
case "mainpage":
shell.CurrentItem = mainpage;
break;
}
}
The CurrentItem is selected on referring the x:Name Property of the ShellItem.
The CurrentItem property can be set in any class through the Shell.Current static property:
Shell.Current.CurrentItem = loginpage;
You should create like this. In App.xaml.cs call the your first ContentPage (Login Page)
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new LoginPage()); // LoginPage is contentPage
}
From LoginPage you can validate the fields and synch with api. Based on the response(success/failure) you can redirect to MainPage(Home Page)
if (loginStatus == "isSuccess") {
// if it ie MasterController/Drawer view/flyout menu/menu view
Application.Current.MainPage = new MasterControllerPage(); // need to create a page in the type 'MasterDetailPage'
//or normal content page use this(here HomePage is your MainPage)
Application.Current.MainPage = new NavigationPage(new HomePage());
}
else
{
// handle error alert
DisplayAlert("Sorry", "Something went wrong in server.", "Ok");
}
For Splash page in Android look at this https://learn.microsoft.com/en-us/xamarin/android/user-interface/splash-screen
For Splash page in iOS look at this https://learn.microsoft.com/en-us/xamarin/ios/app-fundamentals/images-icons/launch-screens?tabs=macos
I want to create two AppShell. One will be the principal, with all my menu for my application. The other one only will have a login page.
So you can switch between one AppShel and the other.
MainPage = new AppShell();
MainPage = new LoginAppShell();
In your App.xaml.cs add the following line:
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new MainPage());
}
Than in your authentication file add:
async void OnLoginButtonClicked(Object sender, EventArgs e)
{
var user = new User
{
PhoneNumber = PhoneNumberEntry.Text,
Password = PasswordEntry.Text
};
if (user.PhoneNumber == Dummy.PhoneNumber && user.Password == Dummy.Password)
{
Application.Current.MainPage = new AppShell();
}
else
{
messagelabel.Text = "Invalid Login";
PasswordEntry.Text = string.Empty;
}
}
This worked for me.. hope it helps you guys too :)
I have implemented this way,
public partial class App : Application
{
private AppShell _mainMenu;
private LoginPage _loginPage;
public static App Instance;
public App()
{
InitializeComponent();
// set singleton
Instance = this;
// Check here for previus-login user
if (SystemEnvironment.UserCurrentSession != null)
{
SwitchToMainMenu();
}
else
{
SwitchToLogin();
}
}
public void SwitchToLogin()
{
_loginPage = null;
_loginPage = new LoginPage();
MainPage = _loginPage; // Go to login page
}
public void SwitchToMainMenu()
{
_mainMenu = null;
_mainMenu = new AppShell();
MainPage = _mainMenu; // Go to logged user page
}
protected override void OnStart()
{
// Handle when your app starts
}
protected override void OnSleep()
{
// Handle when your app sleeps
}
protected override void OnResume()
{
// Handle when your app resumes
}
}
To hide the splash, register, login etc. pages from the AppShell
Flyout menu, you can use a custom FlyoutItem DataTemplateSelector
that returns a view with no height for the corresponding ShellContent
page.
This Medium article by #Matthew Bailey explains really well how to implement it.
For the pages you don't want to show as a menu options, create a DataTemplate that returns a view with no height.
Hidden ShellItem DataTemplate example:
<DataTemplate x:Key="FlyoutHiddenItemTemplate">
<BoxView HeightRequest="0"/>
</DataTemplate>
And return it in the DataTemplateSelector OnSelectTemplate method
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
if(item is BaseShellItem flyoutItem)
{
//Example: If the item has no title? we do not want to show it.
if (string.IsNullOrWhiteSpace(flyoutItem.Title))
return FlyoutHiddenItemTemplate;
}
return NavigationItemTemplate;
}
This AppShell login flow sample by #davidortinau is a good starting point to implement this solution.
The correct way is to only set the MainPage one time like so, to prevent blink.
App.xaml.cs
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
protected override async void OnStart()
{
if (await SecureStorage.Default.GetAsync("_token"))
await Shell.Current.GoToAsync($"//Dashboard");
else
await Shell.Current.GoToAsync($"//SignIn");
}
AppShell.xaml
should have a route to a SplashScreen and a SignInPage,In this order.
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
Shell.FlyoutBehavior="Locked">
<!-- Locked/Flyout -->
<ShellItem x:Name="_splashScreen" Route="SplashScreen" FlyoutItemIsVisible="False">
<ShellContent ContentTemplate="{DataTemplate views:SplashScreen}" />
</ShellItem>
<!-- SplahshScreen is here because SecureStorage is asynchronous and we need
to show something during this time of process. -->
<ShellItem x:Name="_signInPage" Route="SignIn" FlyoutItemIsVisible="False">
<ShellContent ContentTemplate="{DataTemplate views:SignInPage}"/>
</ShellItem>
<FlyoutItem FlyoutDisplayOptions="AsMultipleItems">
<ShellContent Route="Dashboard"
Title="Dashboard"
Icon="menu_dashboard.png"
ContentTemplate="{DataTemplate views:Dashboard}" />
<ShellContent Route="Others"
Title="Others"
Icon="menu_others.png"
ContentTemplate="{DataTemplate views:OthersPage}" />
...
</FlyoutItem>
...
SignInPage.xaml and splashScreen.xaml
should never show the FlyoutMenu or the Tabbar
Shell.FlyoutBehavior="Disabled"
Shell.TabBarIsVisible="False"
And here we go when logged, the FlyoutMenu slide gently on the interface, no blink :)

How to bind to a WPF dependency property to window?

i create a dependency property to close a view from view model,
dependencyProperty:
public static class WindowBehaviors
{
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.RegisterAttached("IsOpen"
, typeof(bool),
typeof(WindowBehaviors),
new UIPropertyMetadata(false, IsOpenChanged));
private static void IsOpenChanged(DependencyObject obj,DependencyPropertyChangedEventArgs args)
{
Window window = Window.GetWindow(obj);
if (window != null && ((bool)args.NewValue))
window.Close();
}
public static bool GetIsOpen(Window target)
{
return (bool)target.GetValue(IsOpenProperty);
}
public static void SetIsOpen(Window target, bool value)
{
target.SetValue(IsOpenProperty, value);
}
}
and use it in my xaml like this:
<window
...
Command:WindowBehaviors.IsOpen="True">
it work's fine,but when i want to bind it to a property in viewModel,it dosen't work,and i guess,it dosen't work because i define the resource later in xaml.
in xaml:
<Window.Resources>
<VVM:myVieModel x:Key="myVieModel"/>
</Window.Resources>
and i don't know what should i do,where should i put this:
Command:WindowBehaviors.IsOpen="{binding Isopen}"
public MainWindow()
{
InitializeComponent();
// DO THIS
this.DataContext = Resources["myVieModel"];
}
You need to bind the data context for the scope where your binding is in. Usually this is fairly high up in your XAML, usually the first element in your form or control.
In your case, the data context beeing a static resource the folllowing should work:
<grid DataContext="{StaticResource myVieModel}">
<!-- the code with the binding goß into here -->
</grid>
Actually this is the same as ebattulga suggests, just the XAML way (no code behind).
Thanks for your helps,i fixed it and here is my solution,
i used to use MVVMToolkit but now i'm useing MVVMlight and as you know in MVVMLight,we just define Application Resources Once in App.xaml.so we can bind all the window's properties simply,hope this can help some people who has the same problem!!
app.xaml
<Application.Resources>
<!--Global View Model Locator-->
<vm:ViewModelLocator x:Key="Locator"
d:IsDataSource="True" />
</Application.Resources>
and in the window(view)
DataContext="{Binding DefaultSpecItemVM, Source={StaticResource Locator}}"
and it works perfect.:D

How to capture button click event of webpage (opened inside WebBrowser Control) in WPF form?

Consider a scenario where I have a WebBrowser Control in WPF application.
A web page is loaded inside WebBrowser Control. The web page contains a button.
The web page is of ASP.NET application.
I want to capture the button click event of the webpage into WPF Form (which hosts WebBrowser Control). Is there any way to achieve this functionality ?
Thanks,
Tapan
Here is code that should do exactly what you want with comments to explain what is going on:
public partial class MainWindow : Window
{
/// <summary>
/// This is a helper class. It appears that we can't mark the Window as ComVisible
/// so instead, we'll use this seperate class to be the C# code that gets called.
/// </summary>
[ComVisible(true)]
public class ComVisibleObjectForScripting
{
public void ButtonClicked()
{
//Do whatever you need to do. For now, we'll just show a message box
MessageBox.Show("Button was clicked in web page");
}
}
public MainWindow()
{
InitializeComponent();
//Pass an instance of our helper class as the target object for scripting
webBrowser1.ObjectForScripting = new ComVisibleObjectForScripting();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
//Navigate to your page somehow
webBrowser1.Navigate("http://www.somewhere.com/");
}
private void webBrowser1_LoadCompleted(object sender, NavigationEventArgs e)
{
//Once the document is loaded, we need to inject some custom JavaScript.
//Here is the JavaScript
var javascript = #"
//This is the JavaScript method that will forward the click to the WPF app
function htmlButtonClicked()
{
//Do any other procession...here we just always call to the WPF app
window.external.ButtonClicked();
}
//Find the button that you want to watch for clicks
var searchButton = document.getElementById('theButton');
//Attach an onclick handler that executes our function
searchButton.attachEvent('onclick',htmlButtonClicked);
";
//Grab the current document and cast it to a type we can use
//NOTE: This interface is defined in the MSHTML COM Component
// You need to add a Reference to it in the Add References window
var doc = (IHTMLDocument2)webBrowser1.Document;
//Once we have the document, execute our JavaScript in it
doc.parentWindow.execScript(javascript);
}
}
Some of this was taken from http://beensoft.blogspot.com/2010/03/two-way-interaction-with-javascript-in.html

Resources