I have a Custom Renderer to enable download and zoom options when a WebView is opened in Xamarin, but even seeting the Source with a URL parameter, the page shows empty screen. What to do?
My WebView.xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:loading="clr-namespace:MPS.Libertas.Mobile.Styles.Frames"
xmlns:behaviors="clr-namespace:MPS.Libertas.Mobile.Behaviors"
x:Class="MPS.Libertas.Mobile.Views.DetalhesDoProcesso.PecasPJe"
Title="{Binding Title}">
<StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" BackgroundColor="#E6E8EA" AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds="0,0,1,1">
<WebView x:Name="wvPecasPJe" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
Navigated="PagOnNavigated"
Navigating="PagOnNavigating"
Source="{Binding Value, Mode=TwoWay}"
/>
</StackLayout>
</ContentPage>
And code setting the Source value behind:
private UrlWebViewSource _Value;
public UrlWebViewSource Value
{
get { return _Value; }
set
{
if (_Value != value)
{
_Value = value;
OnPropertyChanged(new PropertyChangedEventArgs("Value"));
}
}
}
public override async Task InitializeAsync(object navigationData)
{
IsRefreshing = true;
try
{
if (navigationData is string linkPecasPJe)
{
Value = new UrlWebViewSource
{
Url = $"{linkPecasPJe}"
};
Console.WriteLine(Value);
}
else
{
throw new InvalidOperationException("Não foi possível recuperar o link das peças.");
}
}
catch (Exception ex)
{
await DialogService.ShowError(ex, "Erro ao abrir as peças do PJe!", "Voltar", null);
await NavigationService.NavigateToPrevious();
}
finally
{
IsRefreshing = false;
await base.InitializeAsync(navigationData);
}
}
My CustomRenderer:
using Android.App;
using Android.Webkit;
using Xamarin.Forms.Platform.Android;
using Xamarin.Forms;
using Android.Content;
using Xamarin.Essentials;
using System.IO;
using System;
using MPS.Libertas.Mobile.Droid.Renderers;
[assembly: ExportRenderer(typeof(Xamarin.Forms.WebView), typeof(MyWebView))]
namespace MPS.Libertas.Mobile.Droid.Renderers
{
internal class MyWebView : WebViewRenderer
{
public MyWebView(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
if (this.Control != null)
{
var webView = new global::Android.Webkit.WebView(this.Context);
webView.SetWebViewClient(new WebViewClient());
webView.SetWebChromeClient(new WebChromeClient());
WebSettings webSettings = webView.Settings;
webSettings.JavaScriptEnabled = true;
webView.SetDownloadListener(new CustomDownloadListener());
this.SetNativeControl(webView);
var source = e.NewElement.Source as UrlWebViewSource;
if (source != null)
{
webView.LoadUrl(source.Url);
}
}
}
}
public class CustomDownloadListener : Java.Lang.Object, IDownloadListener
{
public void OnDownloadStart(string url, string userAgent, string contentDisposition, string mimetype, long contentLength)
{
try
{
DownloadManager.Request request = new DownloadManager.Request(Android.Net.Uri.Parse(url));
request.AllowScanningByMediaScanner();
request.SetNotificationVisibility(DownloadVisibility.VisibleNotifyCompleted);
// if this path is not create, we can create it.
string thmblibrary = FileSystem.AppDataDirectory + "/download";
if (!Directory.Exists(thmblibrary))
Directory.CreateDirectory(thmblibrary);
request.SetDestinationInExternalFilesDir(Android.App.Application.Context, FileSystem.AppDataDirectory, "download");
DownloadManager dm = (DownloadManager)Android.App.Application.Context.GetSystemService(Android.App.Application.DownloadService);
dm.Enqueue(request);
}
catch (Exception)
{
throw;
}
}
}
}
I noticed that, in the CustomRenderer, the source keeps loading with a null value. But the page should load even then because I am setting the source in the ViewModel, shouldnt it?
Related
We are using WebView in Xamarin.Forms using custom renderer. When we have updated Xamarin.Forms version from 4.7.0.1260 to 5.0.0.1874 in our solution it is showing blank spaces at the end of WebView content. It is working fine in Xamarin.Forms version 4.7.0.1260. We have also tried latest version of Xamarin.Forms but there also getting same issue. Getting issue in both platforms Android and iOS.
Please check the below code for the same.
---------//XAML//---------
<ScrollView>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<local:ExtendedWebViewModel x:Name="AppWebView"
Grid.Row="0"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"/>
</Grid>
</ScrollView>
---------//cs//-----------
// xaml cs file code
AppWebView.Source = new HtmlWebViewSource
{
Html = #"<!DOCTYPE html><html><head><meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0, user-scalable=no'><style> *{ font-size:12px; font-family: Verdana !important; }</style></head><body>" + System.Net.WebUtility.HtmlDecode(content) + #"</body></html>",
};
// ExtendedWebViewModel
using Xamarin.Forms;
namespace EmployeeApp.ViewModel
{
public class ExtendedWebViewModel : WebView
{
}
}
---------//Android Custom Renderer//--------
using Android.Content;
using Android.Webkit;
using EmployeeApp.Droid;
using EmployeeApp.ViewModel;
using Plugin.FirebaseCrashlytics;
using System;
using System.Reflection;
using Xamarin.Essentials;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using WebView = Android.Webkit.WebView;
[assembly: ExportRenderer(typeof(ExtendedWebViewModel), typeof(ExtendedWebViewRenderer))]
namespace EmployeeApp.Droid
{
public class ExtendedWebViewRenderer : WebViewRenderer
{
static ExtendedWebViewModel _xwebView = null;
WebView _webView;
public ExtendedWebViewRenderer(Context context) : base(context)
{
}
class ExtendedWebViewClient : Android.Webkit.WebViewClient
{
public override async void OnPageFinished(WebView view, string url)
{
try
{
if (_xwebView != null)
{
int i = 10;
while (view.ContentHeight == 0 && i-- > 0) // wait here till content is rendered
await System.Threading.Tasks.Task.Delay(100);
_xwebView.HeightRequest = view.ContentHeight;
}
base.OnPageFinished(view, url);
}
catch (Exception ex)
{
CrossFirebaseCrashlytics.Current.Log(string.Format("SessionID : {0}, Pagename : {1}, Methodname : {2}, Error : {3}", App.SessionID, MethodBase.GetCurrentMethod().ReflectedType.FullName, MethodBase.GetCurrentMethod().Name, ex.Message)); CrossFirebaseCrashlytics.Current.RecordException(ex);
}
}
public override bool ShouldOverrideUrlLoading(WebView view, IWebResourceRequest request)
{
bool value = true;
try
{
Android.Net.Uri url = request.Url;
if (Convert.ToString(url).StartsWith("http"))
{
var uri = new Uri(Convert.ToString(url));
Launcher.OpenAsync(uri);
value = true;
}
}
catch (Exception ex)
{
CrossFirebaseCrashlytics.Current.Log(string.Format("SessionID : {0}, Pagename : {1}, Methodname : {2}, Error : {3}", App.SessionID, MethodBase.GetCurrentMethod().ReflectedType.FullName, MethodBase.GetCurrentMethod().Name, ex.Message)); CrossFirebaseCrashlytics.Current.RecordException(ex);
}
return value;
}
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
try
{
base.OnElementChanged(e);
_xwebView = e.NewElement as ExtendedWebViewModel;
_webView = Control;
if (e.OldElement == null)
{
_webView.SetWebViewClient(new ExtendedWebViewClient());
}
}
catch (Exception ex)
{
CrossFirebaseCrashlytics.Current.Log(string.Format("SessionID : {0}, Pagename : {1}, Methodname : {2}, Error : {3}", App.SessionID, MethodBase.GetCurrentMethod().ReflectedType.FullName, MethodBase.GetCurrentMethod().Name, ex.Message)); CrossFirebaseCrashlytics.Current.RecordException(ex);
}
}
}
}
---------//iOS Custom Renderer//--------
using System;
using System.Reflection;
using EmployeeApp.iOS;
using EmployeeApp.ViewModel;
using Plugin.FirebaseCrashlytics;
using UIKit;
using WebKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(ExtendedWebViewModel), typeof(ExtendedWebViewRenderer))]
namespace EmployeeApp.iOS
{
public class ExtendedWebViewRenderer : WkWebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
try
{
base.OnElementChanged(e);
NavigationDelegate = new AppWKNavigationDelegate(this);
}
catch (Exception ex)
{
}
}
}
public class AppWKNavigationDelegate : WKNavigationDelegate
{
readonly ExtendedWebViewRenderer extendedWebViewRenderer;
public AppWKNavigationDelegate(ExtendedWebViewRenderer _extendedWebViewRenderer)
{
try
{
extendedWebViewRenderer = _extendedWebViewRenderer ?? new ExtendedWebViewRenderer();
}
catch (Exception ex)
{
}
}
public override async void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
try
{
if (extendedWebViewRenderer.Element is ExtendedWebViewModel extendedWebViewModel)
{
if (webView != null)
{
//string javascript = string.Format(#"document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust= '{0}'", "250%");
//WKJavascriptEvaluationResult handler = (NSObject result, NSError err) => { };
//webView.EvaluateJavaScript(javascript, handler);
await System.Threading.Tasks.Task.Delay(100); // wait here till content is rendered
if (webView.ScrollView != null)
{
if (webView.ScrollView.ContentSize != null)
{
extendedWebViewModel.HeightRequest = (double)webView.ScrollView.ContentSize.Height;
}
}
}
}
}
catch (Exception ex)
{
}
}
public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action<WKNavigationActionPolicy> decisionHandler)
{
try
{
if (navigationAction != null)
{
if (navigationAction.NavigationType == WKNavigationType.LinkActivated)
{
if (navigationAction.Request != null)
{
if (navigationAction.Request.Url != null)
{
UIApplication.SharedApplication.OpenUrl(navigationAction.Request.Url);
decisionHandler(WKNavigationActionPolicy.Cancel);
}
}
}
else
{
decisionHandler(WKNavigationActionPolicy.Allow);
}
}
}
catch (Exception ex)
{
}
}
}
}
Can anyone please check and give any solution for the same?
I'm having some issue getting my ObservableCollection to bind to alexrainman CarouselView.
After reading some basic articles I created my view model:
public class PostObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
string postOwner = string.Empty;
string id = string.Empty;
string profileimage = string.Empty;
string post = string.Empty;
List<string> postimages = null;
public string PostOwner
{
set
{
if (postOwner != value)
{
postOwner = value;
OnPropertyChanged("PostOwner");
}
}
get
{
return postOwner;
}
}
public string Id {
set
{
if (id != value)
{
id = value;
OnPropertyChanged("Id");
}
}
get
{
return id;
}
}
public string Post
{
set
{
if (post != value)
{
post = value;
OnPropertyChanged("Post");
}
}
get
{
return post;
}
}
public string ProfileImage
{
set
{
if (profileimage != value)
{
profileimage = value;
OnPropertyChanged("ProfileImage") ;
}
}
get
{
return profileimage;
}
}
public List<string> PostImages
{
set
{
if (postimages != value)
{
postimages = value;
OnPropertyChanged("PostImages");
}
}
get
{
return postimages;
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
I retrieve my data via a REST call to my server:
public static bool GetMyPostData(ref ObservableCollection<PostObject> myPosts, string groupid, string apikey)
{
try
{
string newURL = URL + "GetPosts";
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
MultipartFormDataContent formdata = new MultipartFormDataContent
{
{ new StringContent(apikey), "apikey" },
{ new StringContent(groupid), "groupid" }
};
HttpResponseMessage response = client.PostAsync(newURL, formdata).Result; // Blocking call! Program will wait here until a response is received or a timeout occurs.
if (response.IsSuccessStatusCode)
{
try
{
myPosts = response.Content.ReadAsAsync<ObservableCollection<PostObject>>().Result;
}
catch (Exception e)
{
Debug.WriteLine(e);
return false;
}
}
}
return true;
}
catch (Exception ex)
{
Debug.WriteLine(ex);
return false;
}
}
Which works I get my data correctly, now I set up my Binding context like so:
ObservableCollection<PostObject> GroupPosts = new ObservableCollection<PostObject>();
public Posts (GroupInfo ginfo)
{
InitializeComponent ();
GroupTitle.Text = ginfo.Title;
CurrentGroupInfo = ginfo;
GetDataPosts();
BindingContext = GroupPosts;
}
public void GetDataPosts()
{
try
{
GroupPosts.Clear();
if (RestController.GetMyPostData(ref GroupPosts, CurrentGroupInfo.Id.ToString(), apikey))
{
Debug.WriteLine("Data downloaded");
}
}
catch(Exception e)
{
Debug.WriteLine(e.Message);
}
And finally I have my XAML set up like this:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:ImageCircle.Forms.Plugin.Abstractions;assembly=ImageCircle.Forms.Plugin"
xmlns:cv="clr-namespace:CarouselView.FormsPlugin.Abstractions;assembly=CarouselView.FormsPlugin.Abstractions"
NavigationPage.HasNavigationBar="True"
NavigationPage.HasBackButton="False"
NavigationPage.BackButtonTitle="Back"
x:Class="forms.Posts">
<NavigationPage.TitleView>
<StackLayout Orientation="Horizontal" VerticalOptions="Center" Spacing="10" >
<Label x:Name="GroupTitle" TextColor="White" FontSize="Medium"/>
</StackLayout>
</NavigationPage.TitleView>
<ContentPage.ToolbarItems>
<ToolbarItem Name="iconexample" Icon="settings.png" Priority="0" Order="Primary" />
</ContentPage.ToolbarItems>
<ContentPage.Content>
<cv:CarouselViewControl x:Name="carousel"
ItemsSource="{Binding PostImages}"
ShowArrows="true"
ShowIndicators="true"
Orientation="Horizontal">
</cv:CarouselViewControl>
</ContentPage.Content>
</ContentPage>
However I get an error
Unhandled Exception:
System.NullReferenceException: Object reference not set to an instance of an object.
So I'm not sure what I'm missing or I need to read up on this a little more? any help would be great.
You want to do a few changes here:
Change the field definition to a property, you won't be able to bind to the field:
public ObservableCollection<PostObject> GroupPosts { get; } = new ObservableCollection<PostObject>();
If you updating the reference then you have to raise property changed event, so your property definition should look like that:
private ObservableCollection<PostObject> _groupPosts = new ObservableCollection<PostObject>();
public ObservableCollection<PostObject> GroupPosts
{
get { return _groupPosts; }
set
{
_groupPosts = value;
RaisePropertyChanged(.....); // here you should notify your binding that value has changed
}
}
Because you are trying to pass this list by reference (ref parameter), you won't be able to compile it with a property so it's better just to return value from your data provider and then apply it:
GroupPosts.Clear();
var newData = RestController.GetMyPostData(CurrentGroupInfo.Id.ToString(), apikey);
GroupPosts = newData;
it's a bad practice to pass the observable collection to an underlying data provider because it will limit it to operate on UI thread only (otherwise after updating the collection on non-ui thread you can crash the app). But this is a top for another post :)
I have a Forms app that takes a few seconds to populate data when I click on a viewCell.
Is there a way that I can show a circular busy indicator during this time through custom renderers or something like that?
You can implement the same by using ActivityIndicator control.
If you are expecting have busy-indicators on multiple pages, then would recommend to implement this using the ControlTemplate (it also allows you to define overlays if needed).
Page Template
<Application.Resources>
<ResourceDictionary>
<ControlTemplate x:Key="DefaultTemplate">
<Grid>
<!-- page content -->
<ContentPresenter />
<!-- overlay -->
<BoxView BackgroundColor="Black" Opacity="0.5"
IsVisible="{TemplateBinding BindingContext.IsBusy}"/>
<!-- busy indicator with text -->
<Frame HorizontalOptions="Center" VerticalOptions="Center"
IsVisible="{TemplateBinding BindingContext.IsBusy}">
<StackLayout>
<ActivityIndicator IsRunning="{TemplateBinding BindingContext.IsBusy}" />
<Label Text="{TemplateBinding BindingContext.BusyText}" />
</StackLayout>
</Frame>
</Grid>
</ControlTemplate>
</ResourceDictionary>
</Application.Resources>
Sample usage:
XAML - assign template to page
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
ControlTemplate="{StaticResource DefaultTemplate}"
.. >
....
</ContentPage>
View Model
public class BaseViewModel : ObservableObject
{
bool _isBusy;
public bool IsBusy
{
get => _isBusy;
set => SetProperty(ref _isBusy, value);
}
string _busyText = "loading..";
public string BusyText
{
get => _busyText;
set => SetProperty(ref _busyText, value);
}
}
public class TestViewModel : BaseViewModel
{
public ICommand OnTapCommand {
get => new Command(async (obj) =>
{
IsBusy = true;
//do heavy lifting here
await Task.Delay(2000);
IsBusy = false;
});
}
...
You can use Acr.UserDialogs, it's a cross-platform package with busy indicators, dialogs, toasts, etc.
In your case, you need to use Loading.
using (Acr.UserDialogs.UserDialogs.Instance.Loading("your message here"))
{
//your long task here
}
For example...
I accomplished this by creating an activity indicator control that can be used in my entire app. I even made it so that you can change the activity indicator text to show any text that you want such as 'Logging in', 'loading', 'uploading', etc. See my post below. Let me know if you have any questions.
Is it possible to have one Activity indicator for entire app?
you can use a DependencyService to Show and Hide a loading indicator.
You will have to download AndHUD for android and BTProgressHUD for iOS NuGet packages.
DependencyService interface
using MyApp.Helpers;
namespace MyApp
{
interface IHudService
{
void ShowHud(string ProgressText = StaticData.Loading);
void HideHud();
void SetText(string Text);
void SetProgress(double Progress, string ProgressText = "");
}
}
Android Code
using AndroidHUD;
using Android.Views;
using Xamarin.Forms;
using MyApp.Droid;
using MyApp.DependencyServices;
using MyApp.Helpers;
[assembly: Dependency(typeof(DroidHudService))]
namespace MyApp.Droid
{
public class DroidHudService : IHudService
{
#region IHudManager implementation
bool isHudVisible;
public void ShowHud(string ProgressText = StaticData.Loading)
{
Device.BeginInvokeOnMainThread(() =>
{
AndHUD.Shared.Show(Forms.Context, ProgressText, maskType: MaskType.Black);
isHudVisible = true;
});
}
public void HideHud()
{
Device.BeginInvokeOnMainThread(() =>
{
AndHUD.Shared.Dismiss(Forms.Context);
isHudVisible = false;
});
}
public void SetProgress(double Progress, string ProgressText = "")
{
if (!isHudVisible)
return;
Device.BeginInvokeOnMainThread(() =>
{
int progress = (int)(Progress * 100);
AndHUD.Shared.Show(Forms.Context, ProgressText + progress + "%", progress, MaskType.Black);
});
}
public void SetText(string Text)
{
if (!isHudVisible)
return;
Device.BeginInvokeOnMainThread(() =>
{
AndHUD.Shared.Show(Forms.Context, Text, maskType: MaskType.Black);
});
}
Android.Views.View CustomLoadingView(string ProgressText)
{
Android.Views.View loadingView = new Android.Views.View(Forms.Context);
return loadingView;
}
#endregion
}
}
iOS Code
using System;
using BigTed;
using CoreAnimation;
using CoreGraphics;
using MyApp.DependencyServices;
using MyApp.Helpers;
using MyApp.iOS;
using Foundation;
using UIKit;
using Xamarin.Forms;
[assembly: Dependency(typeof(IosHudService))]
namespace MyApp.iOS
{
public class IosHudService : IHudService
{
UIView _load;
bool isHudVisible;
#region IHudManager implementation
public void ShowHud(string ProgressText = StaticData.Loading)
{
isHudVisible = true;
SetText(ProgressText);
}
public void HideHud()
{
Device.BeginInvokeOnMainThread(() =>
{
BTProgressHUD.Dismiss();
if (_load != null)
_load.Hidden = true;
isHudVisible = false;
});
}
public void SetProgress(double Progress, string ProgressText = "")
{
int progress = (int)(Progress * 100);
string text = ProgressText + progress + "%";
SetText(text);
}
public void SetText(string text)
{
if (!isHudVisible)
return;
Device.BeginInvokeOnMainThread(() =>
{
BTProgressHUD.Show(status: text, maskType: ProgressHUD.MaskType.Black);
try
{
lblTitle.Text = text;
UIView[] subView = ProgressHUD.Shared.Subviews;
for (int i = 0; i < subView.Length; i++)
{
subView[i].Hidden = true;
}
_load.Hidden = false;
ProgressHUD.Shared.BringSubviewToFront(_load);
}
catch (Exception ex)
{
Console.WriteLine("IosHudService.cs - SetText() " + ex.Message);
}
});
}
UILabel lblTitle;
UIView CustomLoadingView(string ProgressText)
{
UIView loadingView = new UIView();
loadingView.Frame = new CGRect(0, 0, UIScreen.MainScreen.Bounds.Width, UIScreen.MainScreen.Bounds.Height);
UIImageView imgBg = new UIImageView();
imgBg.Image = UIImage.FromFile("load_bg.png");
imgBg.Frame = new CGRect((loadingView.Frame.Width / 2) - 65, (loadingView.Frame.Height / 2) - 70, 130, 140);
loadingView.Add(imgBg);
UIImageView someImageView = new UIImageView();
someImageView.Frame = new CGRect((loadingView.Frame.Width / 2) - 40, (loadingView.Frame.Height / 2) - 50, 75, 75);
someImageView.AnimationImages = new UIImage[]
{
UIImage.FromBundle("spinner.png"),
};
someImageView.AnimationRepeatCount = nint.MaxValue; // Repeat forever.
someImageView.AnimationDuration = 1.0; // Every 1s.
someImageView.StartAnimating();
CABasicAnimation rotationAnimation = new CABasicAnimation();
rotationAnimation.KeyPath = "transform.rotation.z";
rotationAnimation.To = new NSNumber(Math.PI * 2);
rotationAnimation.Duration = 1;
rotationAnimation.Cumulative = true;
rotationAnimation.RepeatCount = float.PositiveInfinity;
someImageView.Layer.AddAnimation(rotationAnimation, "rotationAnimation");
loadingView.Add(someImageView);
lblTitle = new UILabel();
lblTitle.Text = ProgressText;
lblTitle.Frame = new CGRect(imgBg.Frame.X, someImageView.Frame.Y + someImageView.Frame.Height + 15, 130, 20);
lblTitle.TextAlignment = UITextAlignment.Center;
lblTitle.TextColor = UIColor.White;
lblTitle.AdjustsFontSizeToFitWidth = true;
loadingView.Add(lblTitle);
return loadingView;
}
#endregion
}
}
Show/Hide via DependencyService Method
public static void ShowLoadingIndicator(string progressText = "Loading...")
{
Device.BeginInvokeOnMainThread(() =>
{
DependencyService.Get<IHudService>().ShowHud(progressText);
});
}
public static void HideLoadingIndicator()
{
Device.BeginInvokeOnMainThread(() =>
{
DependencyService.Get<IHudService>().HideHud();
});
}
I have manage my code by Creating Disposable class and use it in ViewModels like this:
public class Busy : IDisposable
{
readonly object _sync = new object();
readonly BaseViewModel _viewModel;
readonly bool _showProgressView;
public Busy(BaseViewModel viewModel, bool showProgressView, string displayMessage = null)
{
try
{
_viewModel = viewModel;
lock (_sync)
{
_viewModel.IsBusy = true;
_showProgressView = showProgressView;
if (_showProgressView)
{
if (string.IsNullOrEmpty(displayMessage))
{
displayMessage = "Loading...";
}
DependencyService.Get<IHudService>().ShowHud(displayMessage);
}
}
}
catch(Exception ex)
{
ex.Track();
}
}
public void Dispose()
{
try
{
lock (_sync)
{
_viewModel.IsBusy = false;
if (_showProgressView)
{
DependencyService.Get<IHudService>().HideHud();
}
}
}
catch(Exception ex)
{
ex.Track();
}
}
}
Show the loader indicater via using instance of Busy class Like this:
using (new Busy(this, true))
{
//Your api or waiting stuff
}
I'm using a MasterDetailPage, where both the Menu and the Content page have a white background. So I need to add a shadow separator to the Content page when the menu is showing. Like this:
The only example I could find is this: https://gist.github.com/SeeD-Seifer/120971b4dda7a785a7f4bda928c9dc2b
I've implemented the code, and the shadow effect works on Labels, Images and other elements. But I cannot get it to work on a NavigationPage.
My code:
ShadowEffect.cs
public class ShadowEffect : RoutingEffect
{
public float Radius { get; set; }
public Color Color { get; set; }
public float DistanceX { get; set; }
public float DistanceY { get; set; }
public ShadowEffect() : base("MyCompany.PanelShadowEffect")
{
}
}
ShadowNavigationPage.cs
public ShadowNavigationPage(Page root) : base(root)
{
}
protected override void OnAppearing()
{
base.OnAppearing();
Effects.Add(new ShadowEffect()
{
Radius = 0,
DistanceX = -20,
DistanceY = 0,
Color = Color.Black
});
}
PanelShadowEffect
[assembly: ResolutionGroupName("MyCompany")]
[assembly: ExportEffect(typeof(PanelShadowEffect), "PanelShadowEffect")]
namespace MyApp.iOS.Renderer
{
public class PanelShadowEffect : PlatformEffect
{
protected override void OnAttached()
{
try
{
var effect = (ShadowEffect)Element.Effects.FirstOrDefault(e => e is ShadowEffect);
if (effect == null)
{
return;
}
var control = Control;
if (control == null)
{
var renderer = Platform.GetRenderer((VisualElement)Element);
control = renderer.ViewController.View;
}
control.Layer.CornerRadius = effect.Radius;
control.Layer.ShadowColor = effect.Color.ToCGColor();
control.Layer.ShadowOffset = new CGSize(effect.DistanceX, effect.DistanceY);
control.Layer.ShadowOpacity = .5f;
control.Layer.MasksToBounds = false;
// This doesn't work either
//Container.Layer.CornerRadius = effect.Radius;
//Container.Layer.ShadowColor = effect.Color.ToCGColor();
//Container.Layer.ShadowOffset = new CGSize(effect.DistanceX, effect.DistanceY);
//Container.Layer.ShadowOpacity = .5f;
//Container.Layer.MasksToBounds = false;
}
catch (Exception ex)
{
Console.WriteLine("Cannot set property on attached control. Error: {0}", ex.Message);
}
}
protected override void OnDetached()
{
}
}
}
I'm pretty sure that you're attaching the effect to a wrong control or in a wrong place. I got it to work by subscribing to the Appearing event of the NavigationPage (as seen in MainPage.xaml) and attaching the effect there.
PanelShadowEffect.cs
[assembly: ResolutionGroupName("MasterDetailPageNavigation")]
[assembly: ExportEffect(typeof(PanelShadowEffect), "PanelShadowEffect")]
namespace MasterDetailPageNavigation.iOS
{
public class PanelShadowEffect : PlatformEffect
{
protected override void OnAttached()
{
try
{
var effect = (ShadowEffect)Element.Effects.FirstOrDefault(e => e is ShadowEffect);
if (effect == null)
{
return;
}
Container.Layer.CornerRadius = effect.Radius;
Container.Layer.ShadowColor = effect.Color.ToCGColor();
Container.Layer.ShadowOffset = new CGSize(effect.DistanceX, effect.DistanceY);
Container.Layer.ShadowOpacity = .5f;
Container.Layer.MasksToBounds = false;
}
catch (Exception ex)
{
Console.WriteLine("Cannot set property on attached control. Error: {0}", ex.Message);
}
}
protected override void OnDetached()
{
}
}
}
MainPage.xaml
<?xml version="1.0" encoding="UTF-8"?>
<MasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:effects="clr-namespace:MasterDetailPageNavigation;assembly=MasterDetailPageNavigation"
xmlns:local="clr-namespace:MasterDetailPageNavigation;assembly=MasterDetailPageNavigation"
x:Class="MasterDetailPageNavigation.MainPage">
<MasterDetailPage.Master>
<local:MasterPage x:Name="masterPage" />
</MasterDetailPage.Master>
<MasterDetailPage.Detail>
<NavigationPage x:Name="NaviPage" Appearing="NavigationPage_Appearing">
<x:Arguments>
<local:ContactsPage />
</x:Arguments>
</NavigationPage>
</MasterDetailPage.Detail>
</MasterDetailPage>
MainPage.xaml.cs
void NavigationPage_Appearing(object sender, System.EventArgs e)
{
NaviPage.Effects.Add(new ShadowEffect()
{
Radius = 0,
DistanceX = -20,
DistanceY = 0,
Color = Color.Black
});
}
Here's the result:
I have a DataTemplateSelector that selects between two different cells. On Android, this template picks cells that are defined as Android xml files. I can confirm that the template selector is working because I have two different color circles showing, and the colors are correct. But my data is not being bound and I am not sure why. I think I am not setting the binding somewhere, but I am not sure where/how to do that.
Here is my page that includes the ListViewwith the DataTemplateSelector. I set the ItemsSourcehere, but I never set the bindings for the different parts of the list items. That is where I do not know what to do.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Pages.Routines.TopLevelRoutinesPage"
xmlns:statics="clr-namespace:MyApp.Statics;assembly=MyApp"
xmlns:controls="clr-namespace:MyApp.Controls;assembly=MyApp">
<ContentPage.Resources>
<ResourceDictionary>
<controls:RoutinesDataTemplateSelector x:Key="RoutinesDataTemplateSelector"></controls:RoutinesDataTemplateSelector>
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Content>
<StackLayout VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand"
Orientation="Vertical"
Spacing="0">
<ListView ItemsSource="{Binding SelectedRoutineTree}"
ItemTemplate="{StaticResource RoutinesDataTemplateSelector}"
x:Name="RoutinesView"
ItemSelected="RoutineClicked"
Margin ="0, 8, 0, 0">
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
The code-behind:
using MyApp.ViewModels;
using MyCloudContracts.DTOs;
using System;
using System.Linq;
using Xamarin.Forms;
namespace MyApp.Pages.Routines
{
public partial class TopLevelRoutinesPage : ContentPage
{
private TopLevelRoutinesViewModel _viewModel;
private string _projCompName;
public TopLevelRoutinesPage(Guid docId, bool fromCompany, string projCompName)
{
InitializeComponent();
_projCompName = projCompName;
Title = _projCompName;
_viewModel = new TopLevelRoutinesViewModel(docId, fromCompany);
BindingContext = _viewModel;
if (Device.OS == TargetPlatform.Android)
RoutinesView.SeparatorVisibility = SeparatorVisibility.None;
}
private async void RoutineClicked(object sender, SelectedItemChangedEventArgs e)
{
//since this is also called when an item is deselected, return if set to null
if (e.SelectedItem == null)
return;
var selectedRoutine = (PublishedDocumentFragmentDTO)e.SelectedItem;
var fragId = selectedRoutine.FragmentId;
var title = selectedRoutine.Title;
var blobIdStr = selectedRoutine.BlobId;
var blobId = new Guid(blobIdStr);
if (selectedRoutine.Children.Any())
{
var routineTree = _viewModel.SelectedRoutineTree;
var subroutinesPage = new SubroutinesPage(routineTree, fragId, title, blobId, _projCompName);
await Navigation.PushAsync(subroutinesPage);
}
else
{
var routinePage = new RoutinePage(title, blobId);
await Navigation.PushAsync(routinePage);
}
//take away selected background
((ListView)sender).SelectedItem = null;
}
}
}
The DataTemplateSelector
using MyApp.Pages.Routines.CustomCells;
using MyCloudContracts.DTOs;
using Xamarin.Forms;
namespace MyApp.Controls
{
class RoutinesDataTemplateSelector : DataTemplateSelector
{
private readonly DataTemplate _folderDataTemplate;
private readonly DataTemplate _routineDataTemplate;
public RoutinesDataTemplateSelector()
{
_folderDataTemplate = new DataTemplate(typeof(FolderViewCell));
_routineDataTemplate = new DataTemplate(typeof(RoutineViewCell));
}
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
var chooser = item as PublishedDocumentFragmentDTO;
if (chooser == null)
return null;
else if (chooser.Children.Length == 0)
{
return _routineDataTemplate;
}
else
{
return _folderDataTemplate;
}
}
}
}
And an example of one of my custom ViewCells. I think this is where I am wrong, but I am not sure why. I make the properties, but I do not know how to set them properly.
using Xamarin.Forms;
namespace MyApp.Pages.Routines.CustomCells
{
public class RoutineViewCell : ViewCell
{
public static readonly BindableProperty TitleProperty =
BindableProperty.Create("Title", typeof(string), typeof(RoutineViewCell), "");
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
}
}
Thanks for the help :)
I found the answer. I needed to override OnBindingContextChanged() in the custom cell file. My working code looks like this now:
using Xamarin.Forms;
namespace MyApp.Pages.Routines.CustomCells
{
public class RoutineViewCell : ViewCell
{
public static readonly BindableProperty TitleProperty =
BindableProperty.Create("Title", typeof(string), typeof(RoutineViewCell), "");
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
protected override void OnBindingContextChanged()
{
this.SetBinding(TitleProperty, "Title");
base.OnBindingContextChanged();
}
}
}