How to intercept WebView Navigating event in ViewModel - xamarin.forms

My app has a WebView for displaying some contact information. It has a link to a website that I want to load externally using Device.OpenUri(). I'm using FreshMvvm and I want to intercept the Navigating event from the WebView in the ViewModel and cancel the default action which would load the external page into the WebView.
I've tried using the Corcav.Behaviors plugin which does call my ViewModel command:
<WebView
HorizontalOptions="Fill"
VerticalOptions="FillAndExpand"
Source="{Binding WebViewSource}">
<b:Interaction.Behaviors>
<b:BehaviorCollection>
<b:EventToCommand
EventName="Navigating"
Command="{Binding NavigatingCommand}"
CommandParameter="{Binding}"/> <!-- what goes here -->
</b:BehaviorCollection>
</b:Interaction.Behaviors>
</WebView>
But I'm not sure what the CommandParameter should be - I need the URI of the link that was tapped, and I don't know how to then prevent the default behaviour from occurring.
Is this the best approach or should I be looking at an alternative?

Having revisited this recently for another project I stumbled across the answer. The updated XAML is:
<WebView
x:Name="webView"
HorizontalOptions="Fill"
VerticalOptions="FillAndExpand"
Source="{Binding WebViewSource}">
<behaviors:Interaction.Behaviors>
<behaviors:BehaviorCollection>
<behaviors:EventToCommand
EventName="Navigating"
Command="{Binding NavigatingCommand}"
PassEventArgument="True" />
</behaviors:BehaviorCollection>
</behaviors:Interaction.Behaviors>
</WebView>
The code in the ViewModel, that matches the tapped url against a list of valid options before opening the link in the device's browser, is:
public Command<WebNavigatingEventArgs> NavigatingCommand
{
get
{
return navigatingCommand ?? (navigatingCommand = new Command<WebNavigatingEventArgs>(
(param) =>
{
if (param != null && -1 < Array.IndexOf(_uris, param.Url))
{
Device.OpenUri(new Uri(param.Url));
param.Cancel = true;
}
},
(param) => true
));
}
}

You can´t navigate with a WebView, you must use a custom render (hybridwebview).
Here is an explanation:
https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/custom-renderer/hybridwebview/

Related

Xamarin Forms: How to show an audio player on all pages of the app?

I have a page for playing songs in my project. When I go back to the previous page after playing a song (without pausing it), it will continue to play in the background, and users can pause it at any time from the notification bar. I am using Plugin.MediaManager (Version: 1.1.1) for the integration of songs.
Current song screens on my App
But we have a new suggestion, like playing the songs in the app itself on top or bottom on all the other pages (like WhatsApp). Our application has more than 100 pages, so adding the audio player on all these pages is a tedious task. So any other tricky way to implement it on all the pages by reusing it or any other better solution for this feature?
The expected feature is like the below one, like WhatsApp
If you have a common TabbedPage such as the screenshot you attached of Whatsapp, you could add at the beginning of its xaml file before the definition of the views:
<NavigationPage.TitleView>
<StackLayout>
...
</StackLayout>
</NavigationPage.TitleView>
That way you will have a common Navigation view on top of all of them with custom data that you could modify in the associated xaml.cs file of that TabbedPage.
If you have a different structure for your views you could check the documentation for Control Templates in Xamarin Forms, and include it in your App.xaml.
Good luck!
We implemented this using a pop-up page.
XAML:
<StackLayout
HorizontalOptions="Center"
VerticalOptions="End">
<mm:VideoView
Grid.Column="0"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
AutoPlay="True"
IsVisible="True"
x:Name="audioplayer"
ShowControls="True">
<mm:VideoView.HeightRequest>
<OnIdiom x:TypeArguments="x:Double">
<OnIdiom.Phone>80</OnIdiom.Phone>
<OnIdiom.Tablet>120</OnIdiom.Tablet>
<OnIdiom.Desktop>80</OnIdiom.Desktop>
</OnIdiom>
</mm:VideoView.HeightRequest>
</mm:VideoView>
</StackLayout>
XAML.CS
public partial class AudioPopupPage : PopupPage
{
public AudioPopupPage(object source)
{
InitializeComponent();
if (source != null)
{
audioplayer.Source = source;
}
}
protected override bool OnBackgroundClicked()
{
BackgroundInputTransparent = true;
return false;
}
}
We call the pop-up page when we leave the song page.
protected override bool OnBackButtonPressed()
{
PopupNavigation.Instance.PushAsync(new Views.AudioPopupPage(CrossMediaManager.Current.State), true);
return base.OnBackButtonPressed();
}
With this approach, this audio player is showing on all pages, and it is working fine on all platforms.

Xamarin Forms switch language at runtime on each page

I'm developing a cross-platform app (android, IOS) which has to be able to switch languages on each page at the press of a button. I'm using an AppShell for navigation and have the button for the language switch in the toolbar of the AppShell. I made resource files: AppResources.resx and AppResources.fr.resx. I reload both the current page I am on and the AppShell when switching, which seems to have the side effect of going back to the first page I have set on my navigation bar. The reload of the AppShell seems to be necessary as when I don't do it the page seems to go on top and there is no more navigation as well as the color resources I have set in the AppShell get removed. I use the below code to switch the language of my app:
private void Language_switch(object sender, EventArgs e)
{
var lang_switch = Lang.Text;
if (lang_switch == "FR")
{
CultureInfo language = new CultureInfo("fr");
Thread.CurrentThread.CurrentUICulture = language;
AppResources.Culture = language;
Application.Current.Properties[key: "LanguageCode"] = "fr_FR";
}
else
{
CultureInfo language = new CultureInfo("");
Thread.CurrentThread.CurrentUICulture = language;
AppResources.Culture = language;
Application.Current.Properties[key: "LanguageCode"] = "nl_NL";
}
Application.Current.MainPage = new Surveys();
Application.Current.MainPage = new AppShell();
}
This code used to work but is not working anymore. I do remember updating Xamarin Forms a bit ago so this might have something to do with the code not working anymore. In XAML I read from the resource files as below:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyOpinion.Views.Surveys"
xmlns:vm="clr-namespace:MyOpinion.ViewModels"
Title="{Binding Title}"
xmlns:resource="clr-namespace:MyOpinion.Resx">
<Label Text="{x:Static resource:AppResources.Openstaande}" TextColor="{StaticResource Text}" FontSize="24" Margin="0,0,0,10"/>
</ContentPage>
you can use the TranslateExtension provided from Xamarin Community Toolkit. You don't need to reload your pages.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
x:Class="MyLittleApp.MainPage">
<StackLayout>
<Label Text="{xct:Translate AppResources.ATranslatedMessage}" />
<Label Text="{xct:Translate AppResources.AnotherTranslatedMessage, StringFormat='#{0}'}" />
</StackLayout>
</ContentPage>
You should Initiliaze it first:
LocalizationResourceManager.Current.PropertyChanged += (_, _) => AppResources.Culture = LocalizationResourceManager.Current.CurrentCulture;
LocalizationResourceManager.Current.Init(AppResources.ResourceManager);
After Language change you call:
LocalizationResourceManager.Current.CurrentCulture = newCulture;
Here is documentation
https://learn.microsoft.com/en-us/xamarin/community-toolkit/extensions/translateextension
https://learn.microsoft.com/en-us/xamarin/community-toolkit/helpers/localizationresourcemanager
As #Michael said i dont think either there is a way to change language without poping to Root. Though i dont recommend his idea of copping the navigation stack then pushing the pages. But i will implement the idea for you.
Note that navigation stack in readonly property.
var navStack = Application.Current.MainPage.Navigation.NavigationStack;
Application.Current.MainPage.Navigation.RemovePage(navStack.First());
Application.Current.MainPage = new AppShell ();
foreach (var item in navStack)
{
await Application.Current.MainPage.Navigation.PushAsync((Page)Activator.CreateInstance(item.GetType()));
}

How to display two text boxes as pop up using DisplayPromptAsync method in xamarin?

When I use -
string result = await DisplayPromptAsync("Question 1", "What's your name?");
It shows only one textbox in the pop-up. But how to display two or more textboxes in the pop-up?
Any help would be greatly appreciated.
As IvanIčin said that you can use Rg.Plugins.Popup to create custom popup.
Firstly, install Rg.Plugins.Popup bu nuget package..., then creating popup page
<pages:PopupPage
x:Class="FormsSample.popup.popup2"
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">
<pages:PopupPage.Content>
<StackLayout
Padding="20,0"
BackgroundColor="CadetBlue"
HorizontalOptions="FillAndExpand"
VerticalOptions="Center">
<Label Text="Question 1" />
<Label Text="this is one question!" />
<Entry />
<Entry />
<Button
x:Name="btnsub"
Clicked="btnsub_Clicked"
Text="subit" />
</StackLayout>
</pages:PopupPage.Content>
</pages:PopupPage>
public partial class popup2 : PopupPage
{
public popup2()
{
InitializeComponent();
}
private void btnsub_Clicked(object sender, EventArgs e)
{
}
}
To call this Popup Page from contentpage button.click event.
private async void btnPopupButton_Clicked(object sender, EventArgs e)
{
await PopupNavigation.Instance.PushAsync(new popup2());
}
You can see the screenshot:
You can't as it is not intended to. You can create a custom pop-up either by using some pop-up plug-in or by creating your custom code based on the native prompts (similar to what Xamarin.Forms do).
Just for the record having one input field is very generous from Xamarin as the native Android or iOS developers don't have such a prompt with the input field out of the box (though it isn't too hard to create it but still it goes much beyond one line of code).

Add element to XAML page whiles in Task.Delay()

I'm building a chatbot app with chat bubbles for incoming and outgoing messages. For the incoming messages, I've given it a Task.Delay() and now I'd like to give it an ActivityIndicator every time before the message pops up (i.e. I want to show the activity indicator whiles the message is being delayed). I've added the activity indicator to the XAML of the incoming messages control;
IncomingMessageItemControl
<ViewCell
x:Class="BluePillApp.Controls.IncomingMessageItemControl"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pancake="clr-namespace:Xamarin.Forms.PancakeView;assembly=Xamarin.Forms.PancakeView"
mc:Ignorable="d">
<Grid x:Name="Gridoo">
<pancake:PancakeView
Margin="10,10,80,10"
Padding="15"
BackgroundColor="#53ffc6"
CornerRadius="20,20,0,20"
HasShadow="False"
HorizontalOptions="StartAndExpand">
<Label
FontSize="Medium"
Text="{Binding Text}"
TextColor="#1a1a1a" />
</pancake:PancakeView>
<ActivityIndicator IsRunning="True" IsVisible="True" />
</Grid>
</ViewCell>
The problem is, in the ChatbotMessagingPage, the send button is pressed then an outgoing message is sent before getting a reply/incoming message and I've done this in MVVM like so;
ChatbotMessagingPageViewModel
//This gets the chatbots response for each message
chatbot.MainUser.ResponseReceived += async (sender, args) =>
{
await Task.Delay(1500);
Messages.Add(new ChatMessageModel() { Text = args.Response.Text, User = App.ChatBot });
};
}
#region CLASS METHODS
/// <summary>
/// This function sends a message
/// </summary>
public void Send()
{
if (!string.IsNullOrEmpty(TextToSend))
{
var msgModel = new ChatMessageModel() { Text = TextToSend, User = App.User };
//This adds a new message to the messages collection
Messages.Add(msgModel);
var result = chatbot.Evaluate(TextToSend);
result.Invoke();
//Removes the text in the Entry after message is sent
TextToSend = string.Empty;
}
}
Everytime I press the send button the ActivityIndicator comes along with the IncomingMessage, I'd like the ActivityIndicator to come first, whiles the IncomingMessage is being delayed.
I'm guessing that that view cell is the message bubble.
When you do:
Messages.Add(new ChatMessageModel() { Text = args.Response.Text, User = App.ChatBot });
Your collection is updated and your ListView or whatever hold those ViewCelss is also updated. The ActivityIndicator is part of the ViewCell so it comes at the same time as the message.
[OPTION 1] Using an additional flag
What you can do is create a flag IsBusy or IsDelay or something and bind the visibility of the ActivityIndicator and Label to it:
<Grid x:Name="Gridoo">
<pancake:PancakeView
Margin="10,10,80,10"
Padding="15"
BackgroundColor="#53ffc6"
CornerRadius="20,20,0,20"
HasShadow="False"
HorizontalOptions="StartAndExpand">
<Label
FontSize="Medium"
Text="{Binding Text}"
TextColor="#1a1a1a"
IsVisible="{Binding IsBusy, Converter={Helpers:InverseBoolConverter}}""> />
</pancake:PancakeView>
<ActivityIndicator x:Name="activityIndicator" IsRunning="True" IsVisible="{Binding IsBusy}" />
</Grid>
Note that I've used a IValueConverter to negate the value for the label. In case you're not familiar with it, check this
What's left is to add the flag in your ViewModel:
IsBusy = true; // this will make the activity indicator visible, but not the Label
// Also note that you first need to add the message
Messages.Add(new ChatMessageModel() { Text = args.Response.Text, User = App.ChatBot });
await Task.Delay(1500);
IsBusy = false; // this will hige the activity indicator visible, and make Label visible
So basically the logic is the following:
You add the message to your chat BUT the actual text is hidden when on the other hand, the activity indicator is visible.
You apply the delay
Delay ends, you change the visibility of both views.
Note that in my example I've not declared where that flag is since I'm not sure how the rest of your code looks like. It could be added to ChatMessageModel or ChatMessageViewModel since you would need a flag for each message.
[OPTION 2] in IncomingMessageItemControl.xaml.cs
A better solution could be to handle it in the code behind of your control. The issue is the same, the activity indicator and the label comes at the same time.
To fix this you can move the delay in IncomingMessageItemControl.xaml.cs.
First, you need to add x:Name to both the activity indicator and the label.
Then you could do:
private async Task ChangeVisibilityAsync()
{
Label.IsVisibe= false;
ActivityIndicator.IsVisible = true;
await Task.Delay(1500);
Label.IsVisibe = true;
ActivityIndicator.IsVisible = false;
}

How to remove the child added to the stackLayout

I am using the scrollView and a stackLayout in it. Adding the View dynamically to the stackLayout. I am getting the following exception when I am trying to remove the View added:
The calling thread cannot access this object because a different
thread owns it.
My code is as following
<ScrollView Orientation="Vertical" VerticalOptions="FillAndExpand"
x:Name="MessagesScrollView">
<StackLayout Padding="7" x:Name="MessagesStackLayout"
HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
</StackLayout>
</ScrollView>
Add the code behind is:
MessagesStackLayout.Children.Clear();
foreach (var chat in messagesList)
{
MessagesStackLayout.Children.Add(new CustomViewCell(chat));
}
I have a feeling it is not running the code on MainThread try the following and see if that works for you:
Device.BeginInvokeOnMainThread(() =>
{
MessagesStackLayout.Children.Add(new CustomViewCell(chat));
});

Resources