I'm calling a service and returning a bunch of latitudes and longitudes which I'm then placing on a map using MapKit.
using MKAnnotationView I'm adding a RightCallOutButton to each annotation.
So I had to create a new MapDelegate. Code below.
If I click on the button I create the app crashes and I get an error from MonoTouch saying the selector is accings omething that has already been GC'd (garbage collected).
So my question would be, where should I set the RightCalloutAccessoryView and where should I create the button, if not in this code below?
public class MapDelegage : MKMapViewDelegate {
protected string _annotationIdentifier = "BasicAnnotation";
public override MKAnnotationView GetViewForAnnotation (MKMapView mapView, NSObject annotation) {
MKAnnotationView annotationView = mapView.DequeueReusableAnnotation(this._annotationIdentifier);
if(annotationView == null) {
annotationView = new MKPinAnnotationView(annotation, this._annotationIdentifier);
} else {
annotationView.Annotation = annotation;
}
annotationView.CanShowCallout = true;
(annotationView as MKPinAnnotationView).AnimatesDrop = true;
(annotationView as MKPinAnnotationView).PinColor = MKPinAnnotationColor.Green;
annotationView.Selected = true;
var button = UIButton.FromType(UIButtonType.DetailDisclosure);
button.TouchUpInside += (sender, e) => {
new UIAlertView("Testing", "Testing Message", null, "Close", null).Show ();
} ;
annotationView.RightCalloutAccessoryView = button;
return annotationView;
}
}
annotationView = new MKPinAnnotationView(annotation, this._annotationIdentifier);
...
var button = UIButton.FromType(UIButtonType.DetailDisclosure);
You should avoid declaring local variables to hold references you expect to outlive the method itself. Once there's no reference to annotationView or button the Garbage Collector (GC) is free to collect them (the managed part) even if it's native counterparts still exists. However when a callback to them is called you'll get a crash.
The easiest solution is to keep a list of them and (at the class level, i.e. a List<MKPinAnnotationView> field) clear the list when you destroy the view. The UIButton should not be necessary since there's a reference between the view and it.
NOTE: work is being done to hide this complexity from developers in future versions of MonoTouch. Sadly you cannot ignore such issues at the moment.
Related
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've written a custom MvxTouchViewPresenter that allows me to show either a SlidingPanel (RootView), or show a MvxTabBarViewController (AuthView).
When my app launches,
if I tell it to load the TabBarView (AuthView), it works as expected.
if I tell it to load the SlidingPanelView (RootView), it also works as expected.
The problem occurs when I load the AuthView and then try to ShowViewModel<RootView>()... basically what happens in this scenario is that I stay at the AuthView, even though I see the CustomPresenter.Show() method has run appropriately.
Here's the method
public override void Show(MvxViewModelRequest request)
{
var viewController = (UIViewController)Mvx.Resolve<IMvxTouchViewCreator>().CreateView(request);
RootController = new UIViewController();
// This needs to be a Tab View
if (request.ViewModelType == typeof(AuthViewModel))
{
_navigationController = new EmptyNavController(viewController);
RootController.AddChildViewController(_navigationController);
RootController.View.AddSubview(_navigationController.View);
}
if (request.ViewModelType == typeof(RootViewModel))
{
_navigationController = new SlidingPanelsNavController(viewController);
RootController.AddChildViewController(_navigationController);
RootController.View.AddSubview(_navigationController.View);
AddSlidingPanel<NavigationFragment>(PanelType.LeftPanel, 280);
}
base.Show(request);
}
And here's a Gist of the complete class
What am I missing in trying to make this work appropriately?
not sure if what I've done is "correct" but it's working for now. I'm still very much open to better answers.
What I've done in order to simply move on from this problem is add a call to SetWindowRootViewController(_navigationController); just before I call base.Show(request)
public override void Show(MvxViewModelRequest request)
{
_navigationController = null;
var viewController = (UIViewController)Mvx.Resolve<IMvxTouchViewCreator>().CreateView(request);
RootController = new UIViewController();
// This needs to be a Tab View
if (request.ViewModelType == typeof(AuthViewModel))
{
_navigationController = new EmptyNavController(viewController);
RootController.AddChildViewController(_navigationController);
RootController.View.AddSubview(_navigationController.View);
}
else if (request.ViewModelType == typeof (RootViewModel))
{
_navigationController = new SlidingPanelsNavController(viewController);
RootController.AddChildViewController(_navigationController);
RootController.View.AddSubview(_navigationController.View);
AddSlidingPanel<NavigationFragment>(PanelType.LeftPanel, 280);
}
else
{
throw new Exception("They View Type you're trying to show isn't currently supported.");
}
// RIGHT HERE
SetWindowRootViewController(_navigationController);
base.Show(request);
}
I've created a new class that inherits from TTDefaultStyleSheet.
public class BlackStyleSheet : TTDefaultStyleSheet
{
public BlackStyleSheet() : base()
{
Console.WriteLine("BlackStyleSheet created.");
}
public override UIColor TabBarTintColor
{
get
{
Console.WriteLine("BlackStyleSheet.TabBarTintColor returned.");
return UIColor.Black;
}
}
[Export ("tabTintColor")]
public override UIColor TabTintColor
{
get
{
Console.WriteLine("BlackStyleSheet.TabTintColor returned.");
return UIColor.Black;
}
}
}
And I set this custom style sheet as the default in my FinishedLaunching method.
public override void FinishedLaunching (UIApplication application)
{
Three20.TTStyleSheet.GlobalStyleSheet = new BlackStyleSheet();
Three20.TTDefaultStyleSheet.GlobalStyleSheet = new BlackStyleSheet();
Console.WriteLine("Three20 style sheet set.");
}
Then, I create the actual TTTabStrip and TTTabItem elements within my own custom UIViewController's ViewDidLoad() method. The TTTabItem objects are declared at the class level instead of the method level.
tab1 = new TTTabItem("1");
tab2 = new TTTabItem("2");
tab3 = new TTTabItem("3");
TabStrip = new TTTabStrip();
TabStrip.Frame = new RectangleF(0,0,View.Frame.Width, 44);
TabStrip.TabItems = NSArray.FromNSObjects(tab1,tab2,tab3);
TabStrip.SelectedTabIndex = 0;
View.AddSubview(TabStrip);
When the TTDefaultStyleSheet.GlobalStyleSheet property is set to the new custom stylesheet, the app crashes. When this property setting is removed, the app runs perfectly, but the tab strip remains grey.
In all forums I've read (none seem to be MonoTouch-specific), they all indicate that creating your own stylesheet, then setting it to the global stylesheet is the way to go. But this doesn't seem to work for me with MonoTouch.
Does anyone have any ideas?
Thank you,
John K.
I tried your example in XCode with Objective-C and I can confirm that this this approach does work. I also tried for myself with MonoTouch and saw the same results you report.
I have found several problems in the Three20 binding code in the past that seem to cause aborts like this. You can try and fix up the existing binding code or create only the bindings you need from Three20 manually.
http://docs.xamarin.com/ios/advanced_topics/binding_objective-c_types
There seems to be something holding a reference to my custom button, MyButton (which inherits from UIButton), causing it not to be garbage collected unless I remove it from the superview. This, in turn, would cause the view controller that it is on to also not be finalized and collected.
In my example, I have my custom button but I also have a standard UIButton on the view controller which does not need to be removed from the superview in order to be collected. What's the difference? Looks pretty similar to me.
See this code. The irrelevant lines were removed for example's sake. Some things to note about the sample:
-MyButton is pretty empty. Just a constructor and nothing else overridden.
-Imagine MyViewController being on a UINavigationController
-LoadView() just creates the buttons, hooks up an event for each and adds it to the view
-Touching _button would push another MyViewController to the nav controller
-I'm doing some reference cleanup when popping view controllers off the nav controller in ViewDidAppear()
-In CleanUpRefs() you'll see that I have to remove _myButton from superview in order for all the objects to be garbage collected. _button, on the other hand does not need to be removed.
-I'm expecting the entire MyViewController to be collected, including all subviews, when popping from the nav controller but commenting out _myButton.RemoveFromSuperview() stops this from happening.
public class MyViewController : UIViewController
{
private UIButton _button;
private MyButton _myButton;
private MyViewController _nextController;
public override void LoadView()
{
base.LoadView();
_button = UIButton.FromType(UIButtonType.RoundedRect);
_button.TouchUpInside += PushNewController;
View.AddSubview(_button);
_myButton = new MyButton();
_myButton.TouchUpInside += MyButtonTouched;
View.AddSubview(_myButton);
}
private void PushNewController(object sender, EventArgs e)
{
_nextController = new MyViewController();
NavigationController.PushViewController(_nextController, true);
}
private void MyButtonTouched(object sender, EventArgs e)
{
Console.WriteLine("MyButton touched");
}
public void CleanUpRefs()
{
//_button.RemoveFromSuperview();
_myButton.RemoveFromSuperview();
// remove reference from hooking up event handler
_button.TouchUpInside -= PushNewController;
_myButton.TouchUpInside -= MyButtonTouched;
_button = null;
_myButton = null;
}
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
if(_nextController != null)
{
_nextController.CleanUpRefs();
_nextController = null;
}
}
}
It seems as if there's something different with the fact that MyButton isn't a straight UIButton in that it is inherited. But then again, why would there be an extra reference count to it that's being removed by calling RemoveFromSuperview() especially when there's a UIButton just like it that doesn't need to be removed?
(I apologize for the really bad layout, stackoverflow seems to have problems laying out bullets right above code snippets)
Update: I filed a bug report with the MonoTouch team. You can download the sample project from there if you want to run it. Bug 92.
The reason for not garbage collecting in that scenario is just a bug in MonoTouch.
The upcoming MonoTouch release will contain a fix for this. If you are in a hurry, you can replace your /Developer/MonoTouch/usr/lib/mono/2.1/monotouch.dll with the copy I placed here:
http://tirania.org/tmp/monotouch.dll
I would make a backup, in case I did something wrong in my work-in-progress library.
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.