Xamarin DialogViewController not disposed when Root is set - c#

I'm having a weird problem. I'm using Subclasses of DialogViewController for a multi-page interview. Each page presents a number of fields that can be edited, and hitting save in the upper right pushes the next page onto the NavigationController. It seemed to be working fine, but it became apparent that backing out and repeating the interview leaks memory. I created a simple test case that properly cleans up the button event for the navigation item. In this example, Dispose is called when the second controller is popped, but only if I don't set Root to something other than null (i.e. this works as expected if I comment the Root = ... line). Here's the code. Please tell me I'm missing something stupid.
public class TestViewController : DialogViewController { int mPage;
public TestViewController (int page) : base (null, true)
{
mPage = page;
Root = new RootElement ("Testing") {
new Section () {
new StringElement ("Page: " + mPage)
}
};
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
NavigationItem.RightBarButtonItem = new UIBarButtonItem (UIBarButtonSystemItem.Play);
}
public override void ViewWillAppear (bool animated)
{
base.ViewWillAppear (animated);
NavigationItem.RightBarButtonItem.Clicked += OnClicked;
}
public override void ViewDidDisappear (bool animated)
{
base.ViewDidDisappear (animated);
NavigationItem.RightBarButtonItem.Clicked -= OnClicked;
}
private void OnClicked(object sender, EventArgs e)
{
NavigationController.PushViewController(new TestViewController(mPage + 1), true);
}
protected override void Dispose (bool disposing)
{
base.Dispose (disposing);
}
}

Related

How to trigger a method from an observable that is out of scope

I have a ListView that changes. This ListView is inside LinearLayout that also has an Icon that shows as a checkmark if the ListView items include an item of a certain type. It shows an "X" if none of the items are of that type.
In the code below, the Console.WriteLine works.
How do I update the Icon (aka call the Redraw function) after a NotifyDataSetChanged has been called on the ListView adapter. The function is outside of the scope of the observer and cannot be called inside the OnChanged.
private void Init () {
view = ((Activity)cx).LayoutInflater.Inflate(Resource.Layout.MyPage, this);
eventsListAdapter?.Dispose();
eventsListAdapter = new EventsAdapter(
context,
EventListDisplay.DefaultView,
dateCurrentlyDisplayed);
var myObserver = new MyDataSetObserver();
eventsListAdapter.RegisterDataSetObserver(myObserver);
}
private void Redraw () {
// UPDATE ICON HERE
}
public class MyDataSetObserver : DataSetObserver
{
public override void OnChanged()
{
base.OnChanged();
Console.WriteLine("Change was observerd");
OnDataChanged(new DataChangedEventArgs() { DataChanged = 1, TimeChanged = DateTime.Now });
// This area is hit, but how do I call the Redraw method above? It is out of scope
}
}
/// EDIT: Something I've Tried THAT WORKS! Anything seem off about it?
private void Init () {
view = ((Activity)cx).LayoutInflater.Inflate(Resource.Layout.MyPage, this);
eventsListAdapter?.Dispose();
eventsListAdapter = new EventsAdapter(
context,
EventListDisplay.DefaultView,
dateCurrentlyDisplayed);
var myObserver = new MyDataSetObserver();
eventsListAdapter.RegisterDataSetObserver(myObserver);
myObserver.DataChanged += OnDataChanged;
}
private void Redraw () {
// UPDATE ICON HERE
}
private void OnDataChanged(object sender, EventArgs e) {
Redraw();
}
// Added the last four event handler pieces
public class MyDataSetObserver : DataSetObserver
{
public override void OnChanged()
{
base.OnChanged();
g.ToastShort("Change was observerd");
}
public event EventHandler DataChanged;
protected virtual void OnDataChanged(EventArgs e)
{
EventHandler handler = DataChanged;
handler?.Invoke(this, e);
}
public delegate void DataChangedEventHandler(object sender, DataChangedEventArgs e);
public class DataChangedEventArgs : EventArgs
{
public int DataChanged { get; set; }
public DateTime TimeChanged { get; set; }
}
}
You can use messaging-center to notify your activity to call Redraw() when OnChanged hit.
The MessagingCenter is a simple way to reduce coupling, especially
between view models. It can be used to send and receive simple
messages or pass an argument between classes. Classes should
unsubscribe from messages they no longer wish to receive.
In the OnChanged(), send a message every time it is hit:
public override void OnChanged()
{
base.OnChanged();
Console.WriteLine("Change was observerd");
// This area is hit, but how do I call the Redraw method above? It is out of scope
MessagingCenter.Send<object>(this, "needRedraw");
}
In your Init(), Subscribe the needRedraw message and call redraw whenever the "needRedraw" message is sent:
private void Init()
{
view = ((Activity)cx).LayoutInflater.Inflate(Resource.Layout.MyPage, this);
eventsListAdapter?.Dispose();
eventsListAdapter = new EventsAdapter(
context,
EventListDisplay.DefaultView,
dateCurrentlyDisplayed);
var myObserver = new MyDataSetObserver();
eventsListAdapter.RegisterDataSetObserver(myObserver);
MessagingCenter.Subscribe<object>(this, "needRedraw", (sender) => {
// do something whenever the "needRedraw" message is sent
Redraw();
});
}
Thank you #Tyddlywink for your comment: "youll need to create [an Event] in your MyDataSetObserver class and fire it"
I used this as a resource for adding Events: https://learn.microsoft.com/en-us/dotnet/standard/events/
Here are the updates I added to trigger my Redraw() function:
private void Init () {
view = ((Activity)cx).LayoutInflater.Inflate(Resource.Layout.MyPage, this);
eventsListAdapter?.Dispose();
eventsListAdapter = new EventsAdapter(
context,
EventListDisplay.DefaultView,
dateCurrentlyDisplayed);
var myObserver = new MyDataSetObserver();
eventsListAdapter.RegisterDataSetObserver(myObserver);
myObserver.DataChanged += OnDataChanged;
}
private void Redraw () {
// UPDATE ICON HERE
}
private void OnDataChanged(object sender, EventArgs e) {
Redraw();
}
public class MyDataSetObserver : DataSetObserver
{
public override void OnChanged()
{
base.OnChanged();
// To be honest, I don't know what int DataChanged wants.. so arbitrarily set it to 1.
OnDataChanged(new DataChangedEventArgs() { DataChanged = 1, TimeChanged = DateTime.Now });
}
public event EventHandler DataChanged;
protected virtual void OnDataChanged(EventArgs e)
{
EventHandler handler = DataChanged;
handler?.Invoke(this, e);
}
public delegate void DataChangedEventHandler(object sender, DataChangedEventArgs e);
public class DataChangedEventArgs : EventArgs
{
public int DataChanged { get; set; }
public DateTime TimeChanged { get; set; }
}
}

How to prevent Xamarin forms, a custom menu and WebView renderer with EvaluateJavascript from freezing?

I have created a custom menu item which appears in the default menu which pops up when selecting text on my custom WebView.
On clicking on the menu item it calls EvaluateJavascript to get the selected WebView text, and then passes the text to another page.
However after performing this action once or twice, some text on certain areas of the screen start to become unresponsive to clicks eg. text on the parts of the WebView become unselectable, clicks on that part of the screen on other pages becomes unresponsive and even the soft keyboard becomes unclickable in some spots. If this continues for a while sometimes my app will then suddenly freeze the entire operating system and I have to soft reset my phone. It appears that there maybe some serious memory leakage going on.
I create my custom menu item in the MainActivity class:
public override void OnActionModeStarted(ActionMode mode)
{
if (Root.IsCurrentPageType<DictPage>() && DictP.IsWebViewFocused())
{
IMenu menu = mode.Menu;
menu.Add("To Notes");
menu.GetItem(0).SetOnMenuItemClickListener(new MyMenuItemOnMenuItemClickListener(this, mode));
}
base.OnActionModeStarted(mode);
}
It is then handled in the Listener class...
public class MyMenuItemOnMenuItemClickListener : Java.Lang.Object, IMenuItemOnMenuItemClickListener
{
private MainActivity mContext;
ActionMode _mode;
public MyMenuItemOnMenuItemClickListener(MainActivity activity, ActionMode mode)
{
this.mContext = activity;
_mode = mode;
}
public bool OnMenuItemClick(IMenuItem item)
{
WEB.CopyToMainNotes();
Device.BeginInvokeOnMainThread(() =>
{
//close menu if clicked
_mode?.Finish();
});
return true;
}
}
...which calls CopyToMainNotes on my derived WebView class and its associated Renderer and EventHandler classes:
public class WebViewEx : Xamarin.Forms.WebView
{
public static WebViewEx WEB;
//Namespace
//YourClass
public event WebViewExEventHandler CallNativeMethodEvent;
public void CallNativeMethod(WebViewExEventType type)
{
WebViewExEventArgs e = new WebViewExEventArgs();
e.EventType = type;
CallNativeMethodEvent?.Invoke(this, e);
}
public WebViewEx()
{
WEB = this;
}
public void CopyToMainNotes()
{
Device.BeginInvokeOnMainThread(() =>
{
CallNativeMethod(WebViewExEventType.copyToMainNotes);
});
}
}
public delegate void WebViewExEventHandler(object sender, WebViewExEventArgs e);
public class WebViewExEventArgs : EventArgs
{
public enum WebViewExEventType { copyToMainNotes };
public WebViewExEventType EventType = WebViewExEventType.copyToMainNotes;
public WebViewExEventArgs() : base()
{
}
}
public class WebViewExRenderer : WebViewRenderer
{
public WebViewExRenderer(Android.Content.Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
if (Control != null)
{
WebViewEx ex = e.NewElement as WebViewEx;
ex.CallNativeMethodEvent += WebViewEx_CallNativeMethodEvent;
}
}
internal class JavascriptCallback : Java.Lang.Object, IValueCallback
{
public JavascriptCallback(Action<string> callback)
{
_callback = callback;
}
private Action<string> _callback;
public void OnReceiveValue(Java.Lang.Object value)
{
_callback?.Invoke(Convert.ToString(value));
}
}
private void WebViewEx_CallNativeMethodEvent(object sender, WebViewExEventArgs e)
{
switch (e.EventType)
{
case WebViewExEventType.copyToMainNotes:
{
CopyToMainNotes();
break;
}
}
}
public void CopyToMainNotes()
{
string script = "(function(){ return window.getSelection().toString()})()";
var response = string.Empty;
Control?.EvaluateJavascript(script, new JavascriptCallback((r) =>
{
response = r;
Device.BeginInvokeOnMainThread(() =>
{
DPage.CopyThisTextToAnotherPage(response.ToString().Trim('\"'));
});
}));
}
}
The CopyToMainNotes method above is where the EvaluateJavascript takes place and the selected text finally gets sent to another page.
Any ideas where I might be going wrong here? Thanks in advance!

OnCreateView Called twice

I'm trying to create an activity with two tabs, one holding FragmentA and one holding FragmentB. Here is how I add the fragments to the Activity:
[Activity(Label = "My App")]
public class MyActivity : Activity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.ConnectionMenu);
ActionBar.NavigationMode = ActionBarNavigationMode.Tabs;
AddTab("A", new FragmentA());
AddTab("B", new FragmentB());
}
private void AddTab(string tabText, Fragment fragment)
{
var tab = ActionBar.NewTab();
tab.SetText(tabText);
tab.TabSelected += (sender, e) =>
{
e.FragmentTransaction.Replace(
Resource.Id.fragmentContainer,
fragment);
};
ActionBar.AddTab(tab);
}
}
When I rotate the orientation I want to keep fields filled out in the fragments the same. I save my data in OnSaveInstanceState and restore the data in OnActivityCreated. However, I'm noticing that the OnCreateView and OnActivityCreated methods are being called twice per rotate. The first time containing my filled in Bundle and the second time with bundle being null.
I assume that my error is in the MyActivity class but if you need more information let me know!
Given you create the fragment in your Activity.OnCreate(), you will always have 2 calls due to creating new ones in the method, and maintaining the old ones in the base.OnCreate(). What you should probably do is instead of always creating these fragments, you can search via a tag or ID for an existing fragment and use those in the Tabs instead.
i.e.
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.ConnectionMenu);
ActionBar.NavigationMode = ActionBarNavigationMode.Tabs;
if(savedInstanceState == null)
{
AddTab("A", new FragmentA());
AddTab("B", new FragmentB());
}
else
{
Fragment a = (FragmentA)SupportFragmentManager.FindFragmentByTag("my_tag_a");
Fragment b = (FragmentB)SupportFragmentManager.FindFragmentByTag("my_tag_b");
AddTab("A", a);
AddTab("B", b);
}
}
I ended up solving the issue. as #JonDouglas said you need to make sure the tab wasn't already loaded before creating a new fragment. To do this the fragment can be loaded from the FragmentManager class using a tag. During the TabSelected event if the fragment was not previously create, a new fragment is created and added to the event FragmentTransaction using the tag. During the TabUnselected event, if the fragment was created then it is detached.
I also added in a Bundle value to hold onto the last active tab.
Here is the code I used to solve the issue.
[Activity(Label = "My App")]
public class MyActivity : Activity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.ConnectionMenu);
ActionBar.NavigationMode = ActionBarNavigationMode.Tabs;
AddTab("A", "a_fragment", () => new FragmentA());
AddTab("B", "b_fragment", () => new FragmentB());
if (savedInstanceState != null)
{
var selectedTab = savedInstanceState.GetInt(
"ActionBar.SelectedNavigationIndex", 0);
ActionBar.SetSelectedNavigationItem(selectedTab);
}
}
protected override void OnSaveInstanceState(Bundle savedInstanceState)
{
base.OnSaveInstanceState(savedInstanceState);
savedInstanceState.PutInt(
"ActionBar.SelectedNavigationIndex",
ActionBar.SelectedNavigationIndex);
}
private void AddTab<TFragment>(
string tabText,
string tag,
Func<TFragment> ctor) where TFragment : Fragment
{
var tab = ActionBar.NewTab();
tab.SetText(tabText);
tab.SetTag(tag);
var fragment = FragmentManager.FindFragmentByTag<TFragment>(tag);
tab.TabSelected += (sender, e) =>
{
if (fragment == null)
{
fragment = ctor.Invoke();
e.FragmentTransaction.Add(
Resource.Id.fragmentContainer,
fragment,
tag);
}
else
{
e.FragmentTransaction.Attach(fragment);
}
};
tab.TabUnselected += (sender, e) =>
{
if (fragment != null)
{
e.FragmentTransaction.Detach(fragment);
}
};
ActionBar.AddTab(tab);
}
}

System.ArgumentException: Value does not fall within the expected range C# when subscribing to event in a different class

I'm developing an Android C# Application using Xamarin. It has a very simple structure: the MainActivity launches and creates instances of classes GUIHandler and Connection. Each class raises different events:
In Connection.cs:
public EventHandler<ConnectionEventArgs> ConnectionMade;
protected virtual void OnConnectionMade()
{
if (ConnectionMade != null) {
ConnectionMade (this, new ConnectionEventArgs ());
}
}
public EventHandler<ConnectionEventArgs> ConnectionFailed;
protected virtual void OnConnectionFailed()
{
if (ConnectionFailed != null) {
ConnectionFailed (this, new ConnectionEventArgs ());
}
}
public EventHandler<ConnectionEventArgs> LostConnection;
protected virtual void OnLostConnection()
{
if (LostConnection != null) {
LostConnection (this, new ConnectionEventArgs ());
}
}
public EventHandler<ConnectionEventArgs> ReceivedData;
protected virtual void OnReceivedData(byte[] _byteDataReceived, int _byteDataReceivedLength)
{
if (ReceivedData != null) {
ReceivedData (this, new ConnectionEventArgs {byteDataReceived = _byteDataReceived, byteDataReceivedLength = _byteDataReceivedLength});
}
}
public EventHandler<ConnectionEventArgs> SentData;
protected virtual void OnSentData()
{
if (SentData != null) {
SentData (this, new ConnectionEventArgs ());
}
}
In GUIHandler.cs:
public EventHandler<GUIHandlerEventArgs> SendButtonClicked;
protected virtual void OnSendButtonClicked()
{
if (SendButtonClicked != null) {
SendButtonClicked (this, new GUIHandlerEventArgs { sendTextEditData = sendEditText.Text });
}
}
The subscriptions are in the MainActivity.cs file, which gets run when the application is executed:
...
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
SetContentView (Resource.Layout.Main);
var guiHandler = new GUIHandler();
var connection = new Connection();
connection.ConnectionMade += guiHandler.OnConnectionMade;
connection.ConnectionFailed += guiHandler.OnConnectionFailed;
connection.LostConnection += guiHandler.OnLostConnection;
connection.ReceivedData += guiHandler.OnReceivedData;
connection.SentData += guiHandler.OnSentData;
// THE CODE BELOW RAISES System.ArgumentException: Value does not fall within expected range DURING RUNTIME.
guiHandler.SendButtonClicked += connection.OnSendButtonClicked;
// However, this line makes the job.
guiHandler.SendButtonClicked += (sender, args) => connection.Send (args.sendTextEditData);
}
I can't find the reason why it raises that Exception there and does not anywhere else. However, doing some debugging I noticed I can't subscribe a public method from connection, but have no idea why. Anyone has any idea?
Edit: this is the definition of OnSendButtonClicked in Connection.cs:
public void OnSendButtonClicked (object sender, GUIHandlerEventArgs args)
{
//Send data
Send(args.sendTextEditData);
}

How to enable/add a BackButton on the NavigationController?

I have tried many ways, but haven't succeeded.
In this way I can create a new UIBarButtonItem and it works, the problem is that it dosent lock like a backButton/ArrowBackButton:
public override void ViewWillAppear (bool animated)
{
base.ViewWillAppear (animated);
this.NavigationItem.LeftBarButtonItem = new UIBarButtonItem ("Tillbaka", UIBarButtonItemStyle.Plain, delegate(object sender, EventArgs e) {
this.NavigationController.PopViewControllerAnimated (true);
});
}
Have tried this, but haven't worked:
public override void ViewWillAppear (bool animated)
{
base.ViewWillAppear (animated);
this.NavigationItem.SetHidesBackButton(false,true);
}
With MonoTouch.Dialog you have to set a "pushing" flag in order for it to show the back button. You can do this in the constructor, as indicated below:
public class MyViewController : DialogViewController
{
public MyViewController
: base(new RootElement("foo"), true)
{
}
}
Can you give some context to this please? How is this ViewController being added to the NavigationController ?
If you use the PushViewController method (as below) then a back button will be automatically added.
var viewController = new UIViewController();
this.NavigationController.PushViewController(viewController, true);

Categories

Resources