android webview not showing web page - xamarin

I am having difficulty displaying a web page in a webview in xamarin forms application.
I started out with eshoponcontainers sample application. which went well till I got to the login stage. When the Authorization request is sent the login page doesnt show up. A test in postman revealed an unathorised client error so I was expecting to see that page loaded in my webview but that is not happening.
My .xaml page is as follows
<AbsoluteLayout
Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="0"
Grid.RowSpan="2"
IsVisible="{Binding IsLogin}">
<WebView
Source="{Binding LoginUrl}"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1"
AbsoluteLayout.LayoutFlags="All">
<WebView.Behaviors>
<OnPlatform x:TypeArguments="Behavior">
<On Platform="iOS, Android">
<On.Value>
<behaviors:EventToCommandBehavior
EventName="Navigating"
EventArgsConverter="{StaticResource WebNavigatingEventArgsConverter}"
Command="{Binding NavigateCommand}" />
</On.Value>
</On>
<On Platform="UWP">
<On.Value>
<behaviors:EventToCommandBehavior
EventName="Navigated"
EventArgsConverter="{StaticResource WebNavigatedEventArgsConverter}"
Command="{Binding NavigateCommand}" />
</On.Value>
</On>
</OnPlatform>
</WebView.Behaviors>
</WebView>
</AbsoluteLayout>
The code behind looks like this
public partial class LoginView : ContentPage
{
private bool _animate;
public LoginView ()
{
InitializeComponent ();
}
protected override async void OnAppearing()
{
var content = this.Content;
this.Content = null;
this.Content = content;
var vm = BindingContext as LoginViewModel;
if (vm != null)
{
vm.InvalidateMock();
if (!vm.IsMock)
{
_animate = true;
await AnimateIn();
}
}
}
protected override void OnDisappearing()
{
_animate = false;
}
public async Task AnimateIn()
{
if (Device.RuntimePlatform == Device.UWP)
{
return;
}
await AnimateItem(Banner, 10500);
}
private async Task AnimateItem(View uiElement, uint duration)
{
try
{
while (_animate)
{
await uiElement.ScaleTo(1.05, duration, Easing.SinInOut);
await Task.WhenAll(
uiElement.FadeTo(1, duration, Easing.SinInOut),
uiElement.LayoutTo(new Rectangle(new Point(0, 0), new Size(uiElement.Width, uiElement.Height))),
uiElement.FadeTo(.9, duration, Easing.SinInOut),
uiElement.ScaleTo(1.15, duration, Easing.SinInOut)
);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
}
I have the following inthe "LoginViewModel"
public class LoginViewModel: ViewModelBase
{
private ValidatableObject<string> _userName;
private ValidatableObject<string> _password;
private bool _isMock;
private bool _isValid;
private bool _isLogin;
private string _authUrl;
private ISettingsService _settingsService;
private IOpenUrlService _openUrlService;
private IIdentityService _identityService;
public LoginViewModel(
ISettingsService settingsService,
IOpenUrlService openUrlService,
IIdentityService identityService)
{
_settingsService = settingsService;
_openUrlService = openUrlService;
_identityService = identityService;
_userName = new ValidatableObject<string>();
_password = new ValidatableObject<string>();
InvalidateMock();
AddValidations();
}
public ValidatableObject<string> UserName
{
get
{
return _userName;
}
set
{
_userName = value;
RaisePropertyChanged(() => UserName);
}
}
public ValidatableObject<string> Password
{
get
{
return _password;
}
set
{
_password = value;
RaisePropertyChanged(() => Password);
}
}
public bool IsMock
{
get
{
return _isMock;
}
set
{
_isMock = value;
RaisePropertyChanged(() => IsMock);
}
}
public bool IsValid
{
get
{
return _isValid;
}
set
{
_isValid = value;
RaisePropertyChanged(() => IsValid);
}
}
public bool IsLogin
{
get
{
return _isLogin;
}
set
{
_isLogin = value;
RaisePropertyChanged(() => IsLogin);
}
}
public string LoginUrl
{
get
{
return _authUrl;
}
set
{
_authUrl = value;
RaisePropertyChanged(() => LoginUrl);
}
}
public ICommand MockSignInCommand => new Command(async () => await MockSignInAsync());
public ICommand SignInCommand => new Command(async () => await SignInAsync());
public ICommand RegisterCommand => new Command(Register);
public ICommand NavigateCommand => new Command<string>(async (url) => await NavigateAsync(url));
public ICommand SettingsCommand => new Command(async () => await SettingsAsync());
public ICommand ValidateUserNameCommand => new Command(() => ValidateUserName());
public ICommand ValidatePasswordCommand => new Command(() => ValidatePassword());
public override Task InitializeAsync(object navigationData)
{
if (navigationData is LogoutParameter)
{
var logoutParameter = (LogoutParameter)navigationData;
if (logoutParameter.Logout)
{
Logout();
}
}
return base.InitializeAsync(navigationData);
}
private async Task MockSignInAsync()
{
IsBusy = true;
IsValid = true;
bool isValid = Validate();
bool isAuthenticated = false;
if (isValid)
{
try
{
await Task.Delay(10);
isAuthenticated = true;
}
catch (Exception ex)
{
Debug.WriteLine($"[SignIn] Error signing in: {ex}");
}
}
else
{
IsValid = false;
}
if (isAuthenticated)
{
_settingsService.AuthAccessToken = GlobalSetting.Instance.AuthToken;
await NavigationService.NavigateToAsync<MainViewModel>();
await NavigationService.RemoveLastFromBackStackAsync();
}
IsBusy = false;
}
private async Task SignInAsync()
{
IsBusy = true;
await Task.Delay(10);
LoginUrl = _identityService.CreateAuthorizationRequest();
IsValid = true;
IsLogin = true;
IsBusy = false;
}
private void Register()
{
_openUrlService.OpenUrl(GlobalSetting.Instance.RegisterWebsite);
}
private void Logout()
{
var authIdToken = _settingsService.AuthIdToken;
var logoutRequest = _identityService.CreateLogoutRequest(authIdToken);
if (!string.IsNullOrEmpty(logoutRequest))
{
// Logout
LoginUrl = logoutRequest;
}
if (_settingsService.UseMocks)
{
_settingsService.AuthAccessToken = string.Empty;
_settingsService.AuthIdToken = string.Empty;
}
_settingsService.UseFakeLocation = false;
}
private async Task NavigateAsync(string url)
{
var unescapedUrl = System.Net.WebUtility.UrlDecode(url);
if (unescapedUrl.Equals(GlobalSetting.Instance.LogoutCallback))
{
_settingsService.AuthAccessToken = string.Empty;
_settingsService.AuthIdToken = string.Empty;
IsLogin = false;
LoginUrl = _identityService.CreateAuthorizationRequest();
}
else if (unescapedUrl.Contains(GlobalSetting.Instance.IdentityCallback))
{
var authResponse = new AuthorizeResponse(url);
if (!string.IsNullOrWhiteSpace(authResponse.Code))
{
var userToken = await _identityService.GetTokenAsync(authResponse.Code);
string accessToken = userToken.AccessToken;
if (!string.IsNullOrWhiteSpace(accessToken))
{
_settingsService.AuthAccessToken = accessToken;
_settingsService.AuthIdToken = authResponse.IdentityToken;
await NavigationService.NavigateToAsync<MainViewModel>();
await NavigationService.RemoveLastFromBackStackAsync();
}
}
}
}
private async Task SettingsAsync()
{
await NavigationService.NavigateToAsync<SettingsViewModel>();
}
private bool Validate()
{
bool isValidUser = ValidateUserName();
bool isValidPassword = ValidatePassword();
return isValidUser && isValidPassword;
}
private bool ValidateUserName()
{
return _userName.Validate();
}
private bool ValidatePassword()
{
return _password.Validate();
}
private void AddValidations()
{
_userName.Validations.Add(new IsNotNullOrEmptyRule<string> { ValidationMessage = "A username is required." });
_password.Validations.Add(new IsNotNullOrEmptyRule<string> { ValidationMessage = "A password is required." });
}
public void InvalidateMock()
{
IsMock = _settingsService.UseMocks;
}
}

Related

Xamarin Forms updating map pins when new pin added

I have a Map in Xamarin Forms that will show a collection of Pins stored in a SQLite database. Its quite simple, the map shows with the pins, there is an Add button at the top which presents with a textbox to give a new pin a Name. The Lat/Lng is populated automatically and when Save is pressed, the new pin should get added to the map.
When the map loads, the pins are not shown on the map. I have created a button to load the pins and they will show when that button is pressed.
A breakpoint in the ViewModel ExecuteLoadPinsCommand() shows that Pins does have pins inside it.
When I create a new pin, again the new pin doesn't show.
Here is the View. Note the toolbar button with the binding Pins.Count, thats the button I click to show the pins.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps"
x:Class="WE.Views.MapPage"
xmlns:vm="clr-namespace:WE.ViewModels"
Title="{Binding Title}">
<ContentPage.BindingContext>
<vm:MapPageViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Text="Add" Command="{Binding AddPinCommand}" />
<ToolbarItem Text="{Binding Pins.Count}" Clicked="ToolbarItem_Clicked" />
</ContentPage.ToolbarItems>
<ContentPage.Content>
<maps:Map x:Name="myMap" ItemsSource="{Binding Pins}" IsShowingUser="True">
<maps:Map.ItemTemplate>
<DataTemplate>
<maps:Pin Label="{Binding Name}" Position="{Binding Position}"/>
</DataTemplate>
</maps:Map.ItemTemplate>
</maps:Map>
</ContentPage.Content>
Here is the ViewModel.
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MapPage : ContentPage
{
MapPageViewModel _viewModel;
public MapPage()
{
InitializeComponent();
BindingContext = _viewModel = new MapPageViewModel();
_viewModel.LoadPinsCommand.Execute(this);
}
protected override async void OnAppearing()
{
base.OnAppearing();
_viewModel.OnAppearing();
await GetCurrentLocation();
}
CancellationTokenSource cts;
async Task GetCurrentLocation()
{
var request = new GeolocationRequest(GeolocationAccuracy.Medium, TimeSpan.FromSeconds(10));
cts = new CancellationTokenSource();
var location = await Geolocation.GetLocationAsync(request, cts.Token);
if (location != null)
{
Console.WriteLine($"Latitude: {location.Latitude}, Longitude: {location.Longitude}, Altitude: {location.Altitude}");
myMap.MoveToRegion(MapSpan.FromCenterAndRadius(new Position(location.Latitude, location.Longitude), Distance.FromMeters(100)));
}
}
private async void ToolbarItem_Clicked(object sender, EventArgs e)
{
UpdatePins();
}
void UpdatePins()
{
foreach (var pin in _viewModel.Pins)
{
Pin newPin = new Pin
{
Label = pin.Name,
Position = new Position(Convert.ToDouble(pin.Latitude), Convert.ToDouble(pin.Longitude)),
Type = PinType.Place
};
myMap.Pins.Add(newPin);
}
}
}
and finally, here is the View Model
public class MapPageViewModel : BaseViewModel
{
private readonly IMapService _mapService;
public int PinCount { get; set; }
public ObservableCollection<MapPinModel> Pins { get; set; }
public Command LoadPinsCommand { get; }
public Command AddPinCommand { get; }
public MapPageViewModel()
{
_mapService = new MapService();
Title = "Map";
Pins = new ObservableCollection<MapPinModel>();
PinCount = 0;
LoadPinsCommand = new Command(async () => await ExecuteLoadPinsCommand());
AddPinCommand = new Command(OnAddPin);
this.LoadPinsCommand.Execute(null);
}
async Task ExecuteLoadPinsCommand()
{
IsBusy = true;
try
{
Pins.Clear();
Pins = await App.Database.GetPins();
Debug.WriteLine("Pins in DB: " + Pins.Count.ToString());
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
public async void OnAppearing()
{
IsBusy = true;
}
private async void OnAddPin(object obj)
{
await Shell.Current.GoToAsync(nameof(AddPinPage));
}
}
Add New Pin
ViewModel
private readonly IMapService _mapService;
private string userid;
private string latitude;
private string longitude;
private DateTime dateadded = DateTime.Now;
private DateTime datefound = DateTime.Now;
private string name;
public AddPinViewModel()
{
SaveCommand = new Command(OnSave, ValidateSave);
CancelCommand = new Command(OnCancel);
this.PropertyChanged +=
(_, __) => SaveCommand.ChangeCanExecute();
_mapService = new MapService();
}
private bool ValidateSave()
{
return !String.IsNullOrWhiteSpace(name);
}
public string UserId
{
get => userid;
set => SetProperty(ref userid, value);
}
public string Latitude
{
get => latitude;
set => SetProperty(ref latitude, value);
}
public string Longitude
{
get => longitude;
set => SetProperty(ref longitude, value);
}
public DateTime DateAdded
{
get => dateadded;
set => SetProperty(ref dateadded, value);
}
public DateTime DateFound
{
get => datefound;
set => SetProperty(ref datefound, value);
}
public string Name
{
get => name;
set => SetProperty(ref name, value);
}
public Command SaveCommand { get; }
public Command CancelCommand { get; }
private async void OnCancel()
{
// This will pop the current page off the navigation stack
await Shell.Current.GoToAsync("..");
}
private async void OnSave()
{
var location = await GetCurrentLocation();
var userid = await Xamarin.Essentials.SecureStorage.GetAsync("userId");
MapPinModel pin = new MapPinModel()
{
Name = Name,
DateAdded = DateTime.Now,
DateFound = DateTime.Now,
Latitude = location.Latitude,
Longitude = location.Longitude
};
await App.Database.AddPin(pin);
await Shell.Current.GoToAsync("..");
}
CancellationTokenSource cts;
async Task<LatLngModel> GetCurrentLocation()
{
LatLngModel model = new LatLngModel();
var request = new GeolocationRequest(GeolocationAccuracy.Medium, TimeSpan.FromSeconds(10));
cts = new CancellationTokenSource();
var location = await Geolocation.GetLocationAsync(request, cts.Token);
if (location != null)
{
model.Latitude = location.Latitude.ToString();
model.Longitude = location.Longitude.ToString();
}
return model;
}

Unable to open asset URL: file:///android_asset/www/sf.min.js xamarin forms

While developing xamarin forms custom webview, when I try to load URL from assets folder, getting below error
[AndroidProtocolHandler] Unable to open asset URL: file:///android_asset/www/sf.min.js
This is my code
ChatbotView.xaml file
<controls:HybridWebView
x:Name="webView"
Uri="TestWebPage.html"
Margin="10"
BackgroundColor="Red"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand" />
This is ChatbotViewModel and HybridWebView class
public class ChatbotViewModel: BaseViewModel
{
public UrlWebViewSource SourceContent { get; set; }
public ChatbotViewModel()
{
var source = new UrlWebViewSource();
var baseUrl = DependencyService.Get<IBaseUrl>().Get();
string filePathUrl = Path.Combine(baseUrl, "TestWebPage.html");
source.Url = filePathUrl;
SourceContent = source;
}
}
public interface IBaseUrl { string Get(); }
public class HybridWebView : WebView
{
Action<string> action;
public static readonly BindableProperty UriProperty = BindableProperty.Create(
propertyName: "Uri",
returnType: typeof(string),
declaringType: typeof(HybridWebView),
defaultValue: default(string));
public string Uri
{
get { return (string)GetValue(UriProperty); }
set { SetValue(UriProperty, value); }
}
public void RegisterAction(Action<string> callback)
{
action = callback;
}
public void Cleanup()
{
action = null;
}
public void InvokeAction(string data)
{
if (action == null || data == null)
{
return;
}
action.Invoke(data);
}
}
This is HybridWebViewRenderer, JavascriptWebViewClient and JSBridge class
public class HybridWebViewRenderer : WebViewRenderer
{
const string JavascriptFunction = "function Chat1(data){jsBridge.invokeAction(data);}";
Context _context;
public HybridWebViewRenderer(Context context) : base(context)
{
_context = context;
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
Control.RemoveJavascriptInterface("jsBridge");
((HybridWebView)Element).Cleanup();
}
if (e.NewElement != null)
{
Control.SetWebViewClient(new JavascriptWebViewClient(this, $"javascript: {JavascriptFunction}"));
Control.AddJavascriptInterface(new JSBridge(this), "jsBridge");
Control.Settings.AllowFileAccess = true;
Control.Settings.JavaScriptEnabled = true;
Control.Settings.SetAppCacheMaxSize(100000000);
Control.Settings.AllowFileAccessFromFileURLs = true;
Control.Settings.AllowUniversalAccessFromFileURLs = true;
Control.Settings.AllowContentAccess = true;
Control.SetWebChromeClient(new WebChromeClient());
Control.SetWebViewClient(new WebViewClient());
var myUlr2 = $"file:///android_asset/TestWebPage.html";
Control.LoadUrl(myUlr2);
}
}
public class JavascriptWebViewClient : FormsWebViewClient
{
string _javascript;
public JavascriptWebViewClient(HybridWebViewRenderer renderer, string javascript) : base(renderer)
{
_javascript = javascript;
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
base.OnPageFinished(view, url);
view.EvaluateJavascript(_javascript, null);
}
}
public class JSBridge : Java.Lang.Object
{
readonly WeakReference<HybridWebViewRenderer> hybridWebViewRenderer;
public JSBridge(HybridWebViewRenderer hybridRenderer)
{
hybridWebViewRenderer = new WeakReference<HybridWebViewRenderer>(hybridRenderer);
}
[JavascriptInterface]
[Export("invokeAction")]
public void InvokeAction(string data)
{
HybridWebViewRenderer hybridRenderer;
if (hybridWebViewRenderer != null && hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
{
((HybridWebView)hybridRenderer.Element).InvokeAction(data);
}
}
}
}
Below is TestWebpage.html file. This file is accessing one of JavaScript file from assets folder
<script type='text/javascript' src='file:///android_asset/www/sf.min.js'></script>
sf.min.js is not getting loaded and throwing error.
<html>
<body>
<button onclick="Chat1()">Submit</button>
<script type='text/javascript' src='file:///android_asset/www/sf.min.js'></script>
<script type='text/javascript'>
function Chat1() {
var initESW = function (gslbBaseURL) {
embedded_svc.settings.displayHelpButton = true; //Or false
embedded_svc.settings.language = ''; //For example, enter 'en' or 'en-US'
embedded_svc.settings.enabledFeatures = ['LiveAgent'];
embedded_svc.settings.entryFeature = 'LiveAgent';
embedded_svc.init(
'https://ulr.my.salesforce.com',
'https://ulr.force.com/visualforce',
gslbBaseURL,
'00D7a00000055uj',
'US_Universities',
{
'baseLiveAgentContentURL': 'https://c.la3-c1cs-cdg.salesforceliveagent.com/content',
'deploymentId': '720008Oqg',
'buttonId': '5730PID',
'baseLiveAgentURL': 'https://d.la3-c1cs-cdg.salesforceliveagent.com/chat',
'eswLiveAgentDevName': 'EmbeddedServiceLiveAgent_Parent0000000jLUAQ_17d9a605e8e',
'isOfflineSupportEnabled': false
}
);
};
if (!window.embedded_svc) {
var s = document.createElement('script');
console.log("Control here1")
var sdxMin = 'https://ulr.salesforce.com/embeddedservice/5.0/esw.min.js/'
console.log("Control here2")
s.src = sdxMin;
console.log("Control here3")
s.onload = function () {
initESW(null);
}
document.body.appendChild(s);
}
else {
initESW('https://service.force.com');
}
}
</script>
</body>
</html>
doc.microsoft webview, this I have followed for development. How can I fix this error ?
Edit 1 : Screenshot of Assets folder
From my perspective,you haven't put the sf.min.js file under Assets/www/sf.min.js folder.When compiling the TestWebpage.html, the system can't detect the js file as a result of the error "[AndroidProtocolHandler] Unable to open asset URL: file:///android_asset/www/sf.min.js".

Xamarin Forms Map not adding pins on new pin created

I have a Xamarin Forms map that shows a collection of Pins. When I add a new location it doesn't show the newly created pin until I click 'Refresh'.
I originally had the AddLocation subscribe in the constructor of the ViewModel and it worked, but added the pin 3 times to the database and map. Its since moving the AddLocation subscribe and unsubscribe to their own methods in the ViewModel and calling then OnAppearing() and OnDisappearing() that the new pin doesnt show.
The map is on a page called ItemPage.xaml
<ContentPage.ToolbarItems>
<ToolbarItem Text="Add" Clicked="ToolbarItem_Clicked" />
<ToolbarItem Text="Refresh" Clicked="ToolbarItem_Clicked_1" />
</ContentPage.ToolbarItems>
<ContentPage.Content>
<!--The map-->
<ContentView Content="{Binding Map}" />
</ContentPage.Content>
and the Code Behind -
[DesignTimeVisible(false)]
public partial class ItemPage : ContentPage
{
ItemViewModel viewModel;
public ItemPage()
{
InitializeComponent();
BindingContext = viewModel = new ItemViewModel();
}
private void Button_Clicked(object sender, EventArgs e)
{
}
protected override void OnAppearing()
{
base.OnAppearing();
}
async void ToolbarItem_Clicked(object sender, EventArgs e)
{
await Navigation.PushModalAsync(new NavigationPage(new NewLocationPage()));
}
private void ToolbarItem_Clicked_1(object sender, EventArgs e)
{
viewModel.LoadLocationsCommand.Execute(null);
}
}
The View Model -
public class ItemViewModel : BaseViewModel
{
public ObservableCollection<MapPinModel> Locations { get; set; }
public ObservableCollection<ItemModel> Items { get; set; }
public Command LoadLocationsCommand { get; set; }
public Command LoadItemsCommand { get; set; }
public Map Map { get; private set; }
public ItemViewModel()
{
Title = "Items";
Locations = new ObservableCollection<MapPinModel>();
Items = new ObservableCollection<ItemModel>();
LoadLocationsCommand = new Command(async () => await ExecuteLoadLocationsCommand());
LoadItemsCommand = new Command(async () => await ExecuteLoadItemsCommand());
Map = new Map(MapSpan.FromCenterAndRadius(
new Xamarin.Forms.Maps.Position(53.70251232638285, -1.8018436431884768),
Distance.FromMiles(0.5)))
{
IsShowingUser = true,
VerticalOptions = LayoutOptions.FillAndExpand
};
this.LoadLocationsCommand.Execute(null);
}
public void UnsubscribeMessages()
{
MessagingCenter.Unsubscribe<NewLocationPage, ItemLocationModel>(this, "AddLocation");
}
public void SubscribeMessages()
{
MessagingCenter.Subscribe<NewLocationPage, ItemLocationModel>(this, "AddLocation", async (obj, item) =>
{
await ItemDataStore.AddItemLocationAsync(item);
});
}
async Task ExecuteLoadItemsCommand()
{
if (IsBusy)
return;
try
{
Items.Clear();
var items = await ItemDataStore.GetItemsAsync(true);
foreach (var item in items)
{
Items.Add(item);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
async Task ExecuteLoadLocationsCommand()
{
Map.Pins.Clear();
var locations = await ItemDataStore.GetLocationsAsync(true);
foreach (Feature feature in locations.Features)
{
if (!feature.Geometry.Type.Equals(GeoJSONObjectType.Point))
continue;
Point point = feature.Geometry as Point;
GeoJSON.Net.Geometry.Position s = point.Coordinates as GeoJSON.Net.Geometry.Position;
MapPinModel pin = new MapPinModel
{
PopupContent = feature.Properties["popupContent"].ToString(),
ScientificName = feature.Properties["scientificname"].ToString(),
ItemDescription = feature.Properties["description"].ToString(),
AddedBy = feature.Properties["username"].ToString(),
Latitude = s.Latitude.ToString(),
Longitude = s.Longitude.ToString()
};
Xamarin.Forms.Maps.Position position = new Xamarin.Forms.Maps.Position(Convert.ToDouble(pin.Latitude), Convert.ToDouble(pin.Longitude));
Pin newPin = new Pin
{
Label = pin.PopupContent,
Type = PinType.Place,
Position = position
};
Map.Pins.Add(newPin);
}
}
}
Finally the New Location page -
[DesignTimeVisible(false)]
public partial class NewLocationPage : ContentPage
{
ItemViewModel viewModel;
public ItemLocationModel Location { get; set; }
public NewLocationPage()
{
InitializeComponent();
Location = new ItemLocationModel();
BindingContext = viewModel = new ItemViewModel();
BindingContext = this;
viewModel.SubscribeMessages();
}
async void Save_Clicked(object sender, EventArgs e)
{
var location = await Geolocation.GetLastKnownLocationAsync();
Location.Latitude = location.Latitude.ToString();
Location.Longitude = location.Longitude.ToString();
Location.DatePosted = DateTime.Now;
Location.ItemId = ((ItemModel)itemsPicker.SelectedItem).Id;
Location.SecretLocation = secretLocation.IsToggled;
MessagingCenter.Send(this, "AddLocation", Location);
await Navigation.PopModalAsync();
Location = null;
}
async void Cancel_Clicked(object sender, EventArgs e)
{
await Navigation.PopModalAsync();
}
protected override void OnAppearing()
{
viewModel.SubscribeMessages();
base.OnAppearing();
if (viewModel.Items.Count == 0)
viewModel.LoadItemsCommand.Execute(null);
itemsPicker.ItemsSource = viewModel.Items;
}
protected override void OnDisappearing()
{
viewModel.LoadLocationsCommand.Execute(null);
viewModel.UnsubscribeMessages();
base.OnDisappearing();
}
}

Xamarin forms Commanding CanExecute does not update Button IsEnabled

I'm trying to apply a style to a button which has been disabled by a command.
I assumed the IsEnabled state was the property that was being triggered by the canexecutechanged event but it seems it does not.
What Button Property is being affected and can I hook into this event so that I can provide a style to the button?
In your viewmodel you can add a property that will lead to enable or disable its buttons. follows an example below.
public Command FacebookLoginCommand { get; set; }
private bool _IsBusy;
public override bool IsBusy
{
get
{
return _IsBusy;
}
set
{
_IsBusy = value;
OnPropertyChanged();
FacebookLoginCommand?.ChangeCanExecute();
GoogleLoginCommand?.ChangeCanExecute();
}
}
public LoginViewModel(IUserDialogs dialogs) : base(dialogs)
{
FacebookLoginCommand = new Command(async () =>
{
using (Dialogs.Loading("Carregando"))
{
IsBusy = true;
await Task.Run(() => new FacebookLoginService(Dialogs).Logar());
await Task.Run(() => Task.Delay(TimeSpan.FromSeconds(3)));
IsBusy = false;
}
}, CanExecute());
private Func<bool> CanExecute()
{
return new Func<bool>(() => !IsBusy);
}
}
Here is the example to login when username length 11 and password at least one.
public class MainViewModel : BaseViewModel
{
public Command LoginIn { get; set; }
public MainViewModel()
{
LoginIn = new Command(async () => await SignIn(), (() => CanExecuteLogin));
}
private string _password;
private string _username;
public string UserName
{
get => _username;
set
{
SetProperty(ref _username, value, nameof(UserName));
SetProperty(ref _canExecuteLogin, IsExecutable(), nameof(CanExecuteLogin));
LoginIn?.ChangeCanExecute();
}
}
public string Password
{
get => _password;
set
{
SetProperty(ref _password, value, nameof(Password));
SetProperty(ref _canExecuteLogin, IsExecutable(), nameof(CanExecuteLogin));
LoginIn?.ChangeCanExecute();
}
}
private bool _canExecuteLogin;
public bool CanExecuteLogin
{
get => _canExecuteLogin;
set => SetProperty(ref _canExecuteLogin, value, nameof(CanExecuteLogin));
}
public bool IsExecutable()
{
if (UserName != null && _password != null)
{
if (UserName.Length == 11 && _password.Length > 0)
return true;
}
return false;
}
private async Task SignIn()
{ //Login Code here }
}

Create an ICommand Bindable property on Xamarin Forms

I have a custom checkbox control that I created with an ICommand property and the corresponding bindable property (my checkbox is a Xamarin.Forms XAML Page), the code is:
CheckBox.xaml
<Image x:Name="imgCheckBox"
WidthRequest="20"
HeightRequest="20"/>
<Label x:Name="lblCheckBox"
TextColor="Black"
VerticalOptions="CenterAndExpand"/>
<TapGestureRecognizer Tapped="OnCheckBoxTapped"/>
CheckBox.xaml.cs
public partial class CheckBox : ContentView
{
private static ImageSource uncheckedImage;
private static ImageSource checkedImage;
public CheckBox()
{
InitializeComponent();
uncheckedImage = ImageSource.FromResource("cbUnchecked.png");
checkedImage = ImageSource.FromResource("cbChecked.png");
imgCheckBox.Source = uncheckedImage;
}
public static readonly BindableProperty IsCheckedProperty =
BindableProperty.Create<CheckBox, bool>(
checkbox =>
checkbox.IsChecked,
false,
propertyChanged: (bindable, oldValue, newValue) =>
{
CheckBox checkbox = (CheckBox)bindable;
EventHandler<bool> eventHandler = checkbox.CheckedChanged;
if (eventHandler != null)
{
eventHandler(checkbox, newValue);
}
});
public bool IsChecked
{
set { SetValue(IsCheckedProperty, value); }
get { return (bool)GetValue(IsCheckedProperty); }
}
void OnCheckBoxTapped(object sender, EventArgs args)
{
IsChecked = !IsChecked;
if (IsChecked)
{
imgCheckBox.Source = checkedImage;
}
else
{
imgCheckBox.Source = uncheckedImage;
}
}
public static readonly BindableProperty CheckBoxCommandProperty =
BindableProperty.Create<CheckBox, ICommand>(
checkbox =>
checkbox.CheckBoxCommand,
null,
BindingMode.TwoWay,
propertyChanged: (bindable, oldValue, newValue) =>
{
CheckBox checkbox = (CheckBox)bindable;
EventHandler<bool> eventHandler = checkbox.CheckedChanged;
if (eventHandler != null)
{
eventHandler(checkbox, checkbox.IsChecked);
}
});
public event EventHandler<bool> CheckedChanged;
public ICommand CheckBoxCommand
{
get { return (ICommand)GetValue(CheckBoxCommandProperty); }
set { SetValue(CheckBoxCommandProperty, value); }
}
}
This checkbox implementation is on another Page called TermsAndConditionsPage, that is also a a Xamarin.Forms XAML Page, the code of the implementation is:
<toolkit:CheckBox Text="{Binding txtCheckBox}"
FontSize="Small"
CheckBoxCommand="{Binding OnCheckBoxTapChanged}"
IsChecked="{Binding IsCheckedChanged, Mode=TwoWay}"/>
<Button Text="Next"
Command="{Binding Next_OnClick}"
IsEnabled="{Binding Next_IsEnabled}"
HorizontalOptions="CenterAndExpand"
Clicked="OnNextClicked"/>
The Code Behind of this page is empty (Constructur with InitializeComponent()).
I also have the ViewModel of this page with this code:
TermsAndConditionsViewModel.cs
private string _txtCheckBox;
public string txtCheckBox
{ get { return _txtCheckBox; }
set
{
_txtCheckBox = value;
OnPropertyChanged("txtCheckBox");
}
}
private bool _Next_IsEnabled;
public bool Next_IsEnabled
{
get { return _Next_IsEnabled; }
set
{
_Next_IsEnabled = value;
OnPropertyChanged("Next_IsEnabled");
}
}
private bool _IsCheckedChanged;
public bool IsCheckedChanged
{
get { return _IsCheckedChanged; }
set
{
_IsCheckedChanged = value;
OnPropertyChanged("IsCheckedChanged");
}
}
public ICommand Next_OnClick { get; set; }
public ICommand OnCheckBoxTapChanged { get; set; }
public TermsAndConditionsViewModel()
{
txtCheckBox = "I agree with the terms and conditions";
Next_OnClick = new Command(NextClicked);
OnCheckBoxTapChanged = new Command(CheckBoxTapped);
}
private void CheckBoxTapped()
{
if (IsCheckedChanged)
{ Next_IsEnabled = true; }
else
{ Next_IsEnabled = false; }
}
private void NextClicked()
{ App.Current.MainPage = new Views.HelloWorld(); }
#region INPC
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{ PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); }
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
Now, the question time: the problem I'm having is the CheckBoxTapped Command is not working, I mean, it doesn't do anything, although the checkbox image changes every time I touch it, it does not change the Next_IsEnabled property of my button. I'd like to know what I am missing here to make this command work properly.
EDIT
What I'm looking for is a Command that behaves similarly to the one that Buttons have.
Thanks all for your time!
Since the original answer is now obsolete, here is the new method:
using System.Windows.Input;
public partial class MyControlExample : ContentView
{
// BindableProperty implementation
public static readonly BindableProperty CommandProperty =
BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(MyControlExample), null);
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
// Helper method for invoking commands safely
public static void Execute(ICommand command)
{
if (command == null) return;
if (command.CanExecute(null))
{
command.Execute(null);
}
}
public MyControlExample()
{
InitializeComponent();
}
// this is the command that gets bound by the control in the view
// (ie. a Button, TapRecognizer, or MR.Gestures)
public Command OnTap => new Command(() => Execute(Command));
}
Something like that (pseudocode):
public class YourClassName : View
{
public YourClassName()
{
var gestureRecognizer = new TapGestureRecognizer();
gestureRecognizer.Tapped += (s, e) => {
if (Command != null && Command.CanExecute(null)) {
Command.Execute(null);
}
};
var label = new Label();
label.GestureRecognizers.Add(gestureRecognizer);
}
public static readonly BindableProperty CommandProperty =
BindableProperty.Create<YourClassName, ICommand>(x => x.Command, null);
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
}

Resources