I'm pretty new to Xamarin and iOS and I am putting together a small POC app to learn.
The app uses a view with a UIWebView in it. I intend to load local web files into it and then manipulate those with a mix of JavaScript and C# (JS bridge) but the UIWebView doesn't function properly. It simply doesn't load the pages. If I uninstall the app and then re-install and run it it often load the web page successfully but in subsequent sessions the web page usually doesn't load, leaving the web view as a white empty area. Just to be sure I have tried to load well known web pages, such as xamarin.com or apple.com. Again, the first time I run it the pages load but in subsequent sessions they don't. The behavior is the same on a physical device (an iPhone 5) and on the simulator.
Are there considerations as to how the UIWebView needs to be used? The ViewController hosting it is a sub view of a SlideoutNavigationController.
I have very little experience with UIWebView so any hints are greatly appreciated.
Thanks
EDIT:
The code that loads the web view:
WebView.LoadFinished += (object sender, EventArgs e) =>
{
UIApplication.SharedApplication.NetworkActivityIndicatorVisible = false; System.Diagnostics.Debug.WriteLine("Web page was loaded");
};
WebView.LoadError += (object sender, UIWebErrorArgs e) =>
{
UIApplication.SharedApplication.NetworkActivityIndicatorVisible = false; System.Diagnostics.Debug.WriteLine(e);
};
WebView.ShouldStartLoad += (UIWebView webView, NSUrlRequest request, UIWebViewNavigationType navigationType) =>
{
return true;
};
WebView.Layer.BorderColor = UIColor.Red.CGColor;
WebView.Layer.BorderWidth = 10;
WebView.LoadRequest(new NSUrlRequest(new NSUrl("http://www.apple.com")));
UIApplication.SharedApplication.NetworkActivityIndicatorVisible = true;
WebView.ScalesPageToFit = true;
Note: The ShouldStartLoad event handler is the only one that gets invoked.
This is what the iOS simulator looks like after page (failed to) load:
Figured it out. It turned out loading WebView with (extremely light) HTML content, such as a HTML string, actually required more time than fetching my own objects from a web service. When the data returned by the web service became available WebView still hadn't completed its loading of the web page. So, as my client code got the web service result there was no web page to decorate and the code got in the way of WebViews normal load operation.
This code fixed the sync problem:
private ManualResetEvent _syncLoadWebPage = new ManualResetEvent(false);
private void loadEmptyWebPage()
{
const string EmptyHtml =
"<html><head></head><body style=\"background: aquamarine\"><H1>EMPTY PAGE</H1></body></html>";
WebView.LoadFinished += (sender, e) => webPageReady();
WebView.LoadStarted += (sender, e) => UIApplication.SharedApplication.NetworkActivityIndicatorVisible = true;
WebView.LoadHtmlString(EmptyHtml, new NSUrl(AppDelegate.Current.Data.Web.LocalWebRoot, true));
WebView.ScalesPageToFit = true;
}
private void webPageReady()
{
_syncLoadWebPage.Set();
}
void whenWebPageIsReady(Action callback)
{
Task.Factory.StartNew(() =>
{
_syncLoadWebPage.WaitOne();
callback();
});
}
void bindMapFile()
{
whenWebPageIsReady(() =>
{
UIApplication.SharedApplication.NetworkActivityIndicatorVisible = false;
var snap = new Snap(new WebViewImpl(WebView));
snap.Image(Model.MapFilePath, "map", 0, 0, Model.MapSize.Width, Model.MapSize.Height);
// todo Decorate stuff with map here ...
});
}
All I had to do was to wait for the web page to finish loading and everything seems to work. I simply drew the wrong conclusions from what I saw.
Silly, really.
Related
After ios 11 upgrade WKWebView does not load my website.
I added the following methods to my renderer:
[Export("webViewWebContentProcessDidTerminate:")]
public virtual void ContentProcessDidTerminate(WKWebView webView)
{
Console.WriteLine("ContentProcessDidTerminate");
}
[Export("webView:didFinishNavigation:")]
public void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
UIApplication.SharedApplication.NetworkActivityIndicatorVisible = false;
Console.WriteLine("DidFinishNavigation");
}
[Export("webView:didFailNavigation:withError:")]
public void DidFailNavigation(WKWebView webView, WKNavigation navigation, NSError error)
{
// If navigation fails, this gets called
Console.WriteLine("DidFailNavigation:" + error.ToString());
}
[Export("webView:didFailProvisionalNavigation:withError:")]
public void DidFailProvisionalNavigation(WKWebView webView, WKNavigation navigation, NSError error)
{
// If navigation fails, this gets called
Console.WriteLine("DidFailProvisionalNavigation" + error.ToString());
}
[Export("webView:didStartProvisionalNavigation:")]
public void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation)
{
UIApplication.SharedApplication.NetworkActivityIndicatorVisible = true;
// When navigation starts, this gets called
Console.WriteLine("DidStartProvisionalNavigation");
}
The application shows didStartProvisionalNavigation but it does not move to the other methods.
I also added the following to my info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
I also tried running my WKWebview against another of my sites and that one works just fine.
In Safari, after attaching it to my WKWebView, I can see that some webapi calls does not seem to complete the request. Yet in some cases the same request does complete but some others does not.
Is anyone having this type of issue?
Any help to solve this would be really appreciated.
Thanks,
Hector
UPDATE:I found the solution
It turns out that in iOS 11 the WKWebView is trying to navigate to "about:blank". I'm not aware of our site having that "page".
Then in my DecidePolicy that page was not allowed and I guess that was also stopping my website from continuing loading normally. I added a condition to enable navigation to "about:blank" and that solved it.
[Export("webView:decidePolicyForNavigationAction:decisionHandler:")]
public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action<WKNavigationActionPolicy> decisionHandler)
{
var navType = navigationAction.NavigationType;
var targetFrame = navigationAction.TargetFrame;
var url = navigationAction.Request.Url;
if (
url.ToString().StartsWith("http") && (targetFrame != null && targetFrame.MainFrame == true)
)
{
decisionHandler(WKNavigationActionPolicy.Allow);
}
else if (
(url.ToString().StartsWith("http") && targetFrame == null)
||
url.ToString().StartsWith("mailto:")
|| url.ToString().StartsWith("tel:")) //Whatever your test happens to be
{
//decisionHandler(WKNavigationActionPolicy.Allow);
Device.BeginInvokeOnMainThread(() =>
{
UIApplication.SharedApplication.OpenUrl(url);
}
);
} // NEED THIS IN IOS 11
else if ( url.ToString().StartsWith("about") )
{
decisionHandler(WKNavigationActionPolicy.Allow);
}
}
Hope this helps someone else. :)
my web application offers a download. Javascript creats at the click the url (it depends on the user input) and the browser should open it, so that the page isn't reloaded.
For that, I think I have to alternatives:
// Alt1:
window.open(pathToFile);
// Alt2:
var downloadFrame = document.getElementById('downloads');
if (downloadFrame === null) {
downloadFrame = document.createElement('iframe');
downloadFrame.id = 'downloads';
downloadFrame.style.display = 'none';
document.body.appendChild(downloadFrame);
}
downloadFrame.src = pathToFile;
Both works under Firefox. Problem with open new window method: If the creation of the file at the server needs more time, the new empty tab will be closed late.
Problem with iframe: If there is an error at the server, no feedback is given.
I think at firefox the iframe is the better solution. But the web application must run with an JavaFX WebView, too. JavaFX haven't a download feature, I have to write it. One possible way is to listen on the location property:
final WebView webView = new WebView();
webView.getEngine().locationProperty().addListener(new ChangeListener<String>() {
#Override public void changed(ObservableValue<? extends String> observableValue, String oldLoc, String newLoc) {
if (newLoc.cotains("/download")) {
FileChooser chooser = new FileChooser();
chooser.setTitle("Save " + newLoc);
File saveFile = chooser.showSaveDialog(webView.getEngine().getScene().getWindow());
if (saveFile != null) {
BufferedInputStream is = null;
BufferedOutputStream os = null;
try {
is = new BufferedInputStream(new URL(newLoc).openStream());
os = new BufferedOutputStream(new FileOutputStream(saveFile));
while ((readBytes = is.read()) != -1) {
os.write(b);
}
} finally {
try { if (is != null) is.close(); } catch (IOException e) {}
try { if (os != null) os.close(); } catch (IOException e) {}
}
}
}
}
}
There are some problems:
The download start depends on a part of the url, because JafaFX supports no access to the http headers (that is bearable)
If the user starts the download with the same url two times, only the first download works (the change event only fires, if the url is new). I can crate unique urls (with #1, #2 and so on at the end). But this is ugly.
Only the "window.open(pathToFile);" method works. Loading an iframe don't fire the change location event of the website. That is expectable but I haven't found the right Listener.
Can someone help me to solve 2. or 3.?
Thank you!
PS: Sorry for my bad english.
edit:
For 2. I found a way. I don't know if it is a good one, if it is performant, if the new webview is deleted or is in the cache after download, ....
And the user don't get an feedback, when some a problem is raised:
webView.getEngine().setCreatePopupHandler(new Callback<PopupFeatures, WebEngine>() {
#Override public WebEngine call(PopupFeatures config) {
final WebView downloader = new WebView();
downloader.getEngine().locationProperty().addListener(/* The Listener from above */);
return downloader.getEngine();
}
}
I think you may just need to use copyURLtoFile to get the file...call that when the location changes or just call that using a registered java class. Something like this:
org.apache.commons.io.FileUtils.copyURLToFile(new URL(newLoc), new File(System.getProperty("user.home")+filename));
Using copyURLToFile the current page doesn't have to serve the file. I think registering the class is probably the easiest way to go... something like this:
PHP Code:
Download $filename
Java (in-line class in your javafx class/window... in this case my javafx window is inside of a jframe):
public class JavaApp {
JFrame cloudFrameREF;
JavaApp(JFrame cloudFrameREF)
{
this.cloudFrameREF = cloudFrameREF;
}
public void getfile(String filename) {
String newLoc = "http://your_web_site.com/send_file.php?filename=" + filename;
org.apache.commons.io.FileUtils.copyURLToFile(new URL(newLoc), new File(System.getProperty("user.home")+filename));
}
}
This part would go in the main javafx class:
Platform.runLater(new Runnable() {
#Override
public void run() {
browser2 = new WebView();
webEngine = browser2.getEngine();
appREF = new JavaApp(cloudFrame);
webEngine.getLoadWorker().stateProperty().addListener(
new ChangeListener<State>() {
#Override public void changed(ObservableValue ov, State oldState, State newState) {
if (newState == Worker.State.SUCCEEDED) {
JSObject win
= (JSObject) webEngine.executeScript("window");
// this next line registers the JavaApp class with the page... you can then call it from javascript using "app.method_name".
win.setMember("app", appREF);
}
}
});
You may not need the frame reference... I was hacking some of my own code to test this out and the ref was useful for other things...
I have run into a problem where it appears Page objects are not being Garbage Collected once they have been navigated away from. I have put together a very basic example of this that demonstrates the issue when using a NavigationPage and the PushAsync method. The page displays the number of 'Alive' pages using a list of weak references:
public class AppNavigationPage
{
private static List<WeakReference> pageRefs = new List<WeakReference>();
public static Page GetMainPage()
{
return new NavigationPage(CreateWeakReferencedPage());
}
private static Page CreateWeakReferencedPage()
{
GC.Collect();
var result = CreatePage();
pageRefs.Add(new WeakReference(result));
// Add a second unreferenced page to prove that the problem only exists
// when pages are actually navigated to/from
pageRefs.Add(new WeakReference(CreatePage()));
GC.Collect();
return result;
}
private static Page CreatePage()
{
var page = new ContentPage();
var contents = new StackLayout();
contents.Children.Add(
new Button
{
Text = "Next Page",
Command = new Command(() => page.Navigation.PushAsync(CreateWeakReferencedPage()))
});
contents.Children.Add(
new Label
{
Text = string.Format(
"References alive at time of creation: {0}",
pageRefs.Count(p => p.IsAlive)),
HorizontalOptions = LayoutOptions.CenterAndExpand
});
page.Content = contents;
return page;
}
}
As you click the Next Page button, a new page is created with a fixed value label showing the number of page references alive at the point this page was created. Each time you click the button you obviously see this number increase by 1. My understanding is that when you click 'back' on the Navigation Page, the view should be popped off the stack and thrown away (allowing it to be GC'd). However, when I run this test code it indicates that after we have gone back, this view is being retained in memory. This can be demonstrated by clicking Next Page a few times until the reference count is at 3. If you then click Back and then Next Page, I believe the reference count should still be 3 (indicating the old page was GC'd before the new one was created) however the new reference count is now 4.
This seems like quite a serious bug in the X-Forms navigation implementation for iOS (I haven't tested this for other platforms), my guess being it is somehow related to the Strong Reference Cycle problem described here: http://developer.xamarin.com/guides/cross-platform/application_fundamentals/memory_perf_best_practices/
Has anyone else encountered this and/or come up with a solution/workaround for it? Would anyone else agree this is a bug?
As an addition, I did a second example that doesn't involve a NavigationPage (so has to use PushModalAsync instead) and found I had the same problem, so this issue doesn't look to be unique to NavigationPage navigation. For reference the code for that (very similar) test is here:
public class AppModal
{
private static List<WeakReference> pageRefs = new List<WeakReference>();
public static Page GetMainPage()
{
return CreateWeakReferencedPage();
}
private static Page CreateWeakReferencedPage()
{
GC.Collect();
var result = CreatePage();
pageRefs.Add(new WeakReference(result));
// Add a second unreferenced page to prove that the problem only exists
// when pages are actually navigated to/from
pageRefs.Add(new WeakReference(CreatePage()));
GC.Collect();
return result;
}
private static Page CreatePage()
{
var page = new ContentPage();
var contents = new StackLayout();
contents.Children.Add(
new Button
{
Text = "Next Page",
Command = new Command(() => page.Navigation.PushModalAsync(CreateWeakReferencedPage()))
});
contents.Children.Add(
new Button
{
Text = "Close",
Command = new Command(() => page.Navigation.PopModalAsync())
});
contents.Children.Add(
new Label
{
Text = string.Format(
"References alive at time of creation: {0}",
pageRefs.Count(p => p.IsAlive)),
HorizontalOptions = LayoutOptions.CenterAndExpand
});
page.Content = contents;
return page;
}
}
I think what you are seeing is a side effect of async navigation, not memory leak. Instead of WeakReferences you might opt for a finalizer instead and create instances of MyPage (instead of ContentPage).
public class MyPage: ContentPage
{
private static int count;
public MyPage()
{
count++;
Debug.WriteLine("Created total " + count);
}
~MyPage()
{
count--;
Debug.WriteLine("Finalizer, remaining " + count);
}
}
Next trick is to add a delayed GC.Collect() call, like:
private static Page CreateWeakReferencedPage()
{
GC.Collect();
var result = CreatePage();
var ignore = DelayedGCAsync();
return result;
}
private static async Task DelayedGCAsync()
{
await Task.Delay(2000);
GC.Collect();
}
You will note that instances get garbage collected within this delayed collection (output window).
As per Xamarin GarbageCollector: I doubt that it has serious flaws. A minor bug here and there but not that huge. That said, dealing with garbage collections in Android is particularly tricky because there are two of those - Dalvik's and Xamarin's. But that is another story.
I am trying to display a webpage from codenameone application. It works fine in Iphone but not in andriod mobile.
public void showLoginForm()
{
final Form loginForm = new Form("Login");
loginForm.setUIID("Form1");
loginForm.setLayout(new BorderLayout());
loginForm.setScrollable(false);
try
{
WebBrowser browser = new WebBrowser()
{
//Overrides onStart and onLoad methods to load progress bars for page transitions.
};
browser.setURL(appsGlobalSettings.get(URL_KEY));
loginForm.addComponent(BorderLayout.CENTER,browser);
loginForm.show();
}
catch(Exception e)
{
e.printStackTrace();
}
}
Check your URL. I suggest removing the override code and placing Google.com hardcoded as a URL and proceeding from there. Since it doesn't work in the simulator either make sure you are using Java 7 with JavaFX enabled, with that case it should show a browser and should work for a proper web address.
Shai...It took some time to collect device logs.
But even that is intriguing.
I tried in two ways
No overridden methods in WebBrowser class and http://www.google.co.in as URL.
public void showLoginForm()
{
InfiniteProgress inf = new InfiniteProgress();
Dialog progress = inf.showInifiniteBlocking();
final Form loginForm = new Form("Login");
loginForm.setUIID("Form1");
loginForm.setLayout(new BorderLayout());
try
{
//Log.p("Inside showLoginForm method");
WebBrowser browser = new WebBrowser()
{
};
//browser.setURL(appsGlobalSettings.get(URL_KEY));
browser.setURL("http://www.google.co.in");
//Log.p("Set Broswer url");
loginForm.addComponent(BorderLayout.CENTER,browser);
//Log.sendLog();
loginForm.show();
}
catch(Exception e)
{
e.printStackTrace();
//Log.p(e.toString());
}
}
Result - the same blank screen.
Same code with added Log.p and Log.send() statements.
Result - it works fine
Is this due to some race condition?
I developed an outlook add in (custom task pane), with web browser in the user control.
All the things working well beside the backspace or the delete button when I am writing something in text box in the web browser, I can't use those keys, am I missing something?
I am a few years late to the party but I managed to fix this. The easiest way to fix this is to ensure proper focus is given to the input fields, so you will need to be able to run your own javascript on whatever page is being loaded.
The javascript I run on the page is as follows (using jQuery):
$(document).on("click", function (e) {
// first let the add-in give focus to our CustomTaskPane
window.external.focus();
// then in our web browser give focus to whatever element was clicked on
$(e.target).focus();
});
the window.external variable contains code run from the plugin (c# or VB I assume) which is exposed so we can interact from web page back to the add-in.
In the add-in code for the custom taskpane set the context of window.external:
// event when webBrowser is finished loading document
private void webBrowser1_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
// sets context of window.external to functions defined on this context
webBrowser1.ObjectForScripting = this;
}
And a public method for focusing:
// can be called by the web browser as window.external.focus()
public void focus()
{
this.Focus();
}
This worked for me, and I hope it helps others. Although do note that this probably doesn't work if the user keyboard navigates using tab, but you can either extend this code for that use case, or safely assume that the average outlook user will have his hand glued to the mouse.
Ok I solved the problem ,
The problem is that the custom task pane in not always gets fucos from the outlook.
So, I raised an event every time that there is "onclick" for all the pane, and then forced the pane to be in focus.
spent a lot of time trying to get this working in Outlook v16.0.13801.20288 the above did not work for me. I ended up with this working code.
Create a user control and add your webbrowser control to it then customize the .cs as below
private void CreateTaskPane() {
MyWinFormUserControl webBrowser = new MyWinFormUserControl();
webBrowser.webBrowser3.Url = new Uri("https://google.com");
webBrowser.webBrowser3.Width = 500;
webBrowser.webBrowser3.Dock = DockStyle.Fill;
webBrowser.webBrowser3.Visible = true;
webBrowser.Width = 500;
webBrowser.Dock = DockStyle.Fill;
webBrowser.Visible = true;
this.CRMTaskPaneControl = CustomTaskPanes.Add(webBrowser, "My App");
//Components.WebViewContainerWPFUserControl webView = (Components.WebViewContainerWPFUserControl)_eh.Child;
//webView.webview.Source = new Uri("https://localhost:3000");
this.CRMTaskPaneControl.Width = 500;
System.Windows.Forms.Application.DoEvents();
this.CRMTaskPaneControl.Control.Focus();
this.CRMTaskPane.Visible = true;
}
public partial class MyWinFormUserControl : UserControl
{
public WebBrowser webBrowser3;
public System.Windows.Forms.WebBrowser webBrowser1;
public MyWinFormUserControl()
{
InitializeComponent();
}
private void InitializeComponent()
{
this.webBrowser3 = new System.Windows.Forms.WebBrowser();
this.SuspendLayout();
//
// webBrowser3
//
this.webBrowser3.Dock = System.Windows.Forms.DockStyle.Fill;
this.webBrowser3.Location = new System.Drawing.Point(0, 0);
this.webBrowser3.MinimumSize = new System.Drawing.Size(20, 20);
this.webBrowser3.Name = "webBrowser3";
this.webBrowser3.Size = new System.Drawing.Size(500, 749);
this.webBrowser3.TabIndex = 0;
this.webBrowser3.DocumentCompleted += new System.Windows.Forms.WebBrowserDocumentCompletedEventHandler(this.webBrowser3_DocumentCompleted);
//
// MyWinFormUserControl
//
this.Controls.Add(this.webBrowser3);
this.Name = "MyWinFormUserControl";
this.Size = new System.Drawing.Size(500, 749);
this.Load += new System.EventHandler(this.MyWinFormUserControl_Load);
this.ResumeLayout(false);
}
void webBrowser3_DocumentCompleted(object sender, System.Windows.Forms.WebBrowserDocumentCompletedEventArgs e)
{
HtmlDocument doc;
doc = webBrowser3.Document;
doc.Click += doc_Click;
}
void doc_Click(object sender, HtmlElementEventArgs e)
{
this.Focus(); // force user control to have the focus
HtmlElement elem = webBrowser3.Document.GetElementFromPoint(e.ClientMousePosition);
elem.Focus(); // then let the clicked control to have focus
}
private void MyWinFormUserControl_Load(object sender, EventArgs e)
{
//Control loaded
}
Turns out this is an easy issue to fix.
Just write
class MyBrowser : WebBrowser {}
Then use MyBrowser instead of the .NET one.