I have some problems running my app on some old Androiddevices, and I therefore downloaded a trail of Visual Studio Professionel, as it has Diagnostics Tools.
I tried doing some simple stuff in my app, and I find it is scaring, that Xamarin.Forms.BindableProperty+BindablePropertyContext takes a size (in bytes of course) of 2.196.088 in UWP, which you can see at the following screendump.
.
In the example I have justed navigated through 5 pages. On 2 of the pages there are ListViews, and one of them have been cleared 3 times, and filled with new data.
So do I have to call GC.Collect() after clearing the ListView?
I've had a similar issue - navigating through pages a couple of times caused an OutOfMemoryException. For me the solution was to implement custom render for page with explicit Dispose() call.
public class CustomPageRenderer : PageRenderer
{
private NavigationPage _navigationPage;
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);
_navigationPage = GetNavigationPage(Element);
SubscribeToPopped(_navigationPage);
}
private void SubscribeToPopped(NavigationPage navigationPage)
{
if (navigationPage == null)
{
return;
}
navigationPage.Popped += OnPagePopped;
}
protected override void Dispose(bool disposing)
{
Log.Info("===========Dispose called===========");
base.Dispose(disposing);
}
private void OnPagePopped(object sender, NavigationEventArgs args)
{
if (args.Page != Element)
{
return;
}
Dispose(true);
_navigationPage.Popped -= OnPagePopped;
}
private static NavigationPage GetNavigationPage(Element element)
{
if (element == null)
{
return null;
}
while (true)
{
if (element.Parent == null || element.Parent.GetType() == typeof(NavigationPage))
{
return element.Parent as NavigationPage;
}
element = element.Parent;
}
}
}
You can also take a look here but you need to be careful with disposing images, it may cause some problems if their parent page is in navigation stack and you want to go back.
Related
I'm currently having an issue where views in a RecycleView are being disposed, despite the Dispose function of the RecyclerView not being called (checked by setting variable) nor it being recycled (also checked by setting a variable).
The main issue: MvxPropertyChangedListener not being cleared. The listener then tries to perform an operation on the disposed view, causing an issue. No matter where I call listener.Clear() (from onViewRecycled in the RecycleView.Adapter, from Dispose, before we initially bind the ViewModel), I still get 'object is disposed' exception.
A try-catch around the view in question 'fixes' the issue, but doesn't solve the root problem.
Question:
In Xamarin, how do I track these RecycleViews? Through my try-catch, I can see which ones have the issue (which is why I can track whether it's disposed, recycled etc.) and the associated ViewModel, but do not then know how to track why and where these inner view objects are disposed?
Note: I have more experience with iOS dev. My Android understanding is fair but lacking atm.
Edit: Code provided.
public class CarouselViewHolder : BaseCarouselViewHolder
{
private SimpleProgressBar _progressBar;
// tile listener
private MvxPropertyChangedListener _tileListener;
private CarouselTileViewModel Tile => (Item as CarouselTileViewModel);
public SingleCarouselItemViewHolder(View v) : base(v)
{
_progressBar = v.FindViewById<BasicProgressBar>(Resource.Id.progressBar);
}
public override void PrepareForReuse()
{
ClearTileListener();
base.PrepareForReuse();
}
public void SetTile(CarouselTileViewModel tile)
{
// keep the item
Item = tile;
// progress bar
RefreshProgressBar ();
// listeners
SetupTileListener();
}
private void SetupTileListener()
{
ClearTileListener();
// setup the listener for the item
if(Item != null && Item is CarouselTileViewModel)
{
_tileListener = new MvxPropertyChangedListener(Item as CarouselTileViewModel);
_tileListener.Listen (() => (Item as CarouselTileViewModel).Progress, RefreshProgressBar);
}
}
private void RefreshProgressBar ()
{
try
{
if (_progressBar != null)
{
_progressBar.Visibility = Tile.Progress > 0 ? ViewStates.Visible : ViewStates.Gone;
_progressBar.SetProgress(Tile.Progress);
}
}
catch (ObjectDisposedException e)
{
Phx.TaggedError ("BasicProgressBar already disposed", "Exception {0}", e);
}
}
private void ClearTileListener()
{
// kill any old listeners
if(_tileListener != null)
{
_tileListener.Clear();
_tileListener.Dispose();
_tileListener = null;
}
}
public override void OnAttachedToWindow()
{
}
public override void OnDetachedFromWindow()
{
}
protected override void Dispose(bool disposing)
{
ClearTileListener();
base.Dispose(disposing);
}
}
public class CarouselItemAdapter : BaseCarouselAdapter<CarouselViewModel>
{
.
.
.
public override void OnViewAttachedToWindow(Java.Lang.Object holder)
{
base.OnViewAttachedToWindow(holder);
if(holder is CarouselViewHolder)
{
(holder as CarouselViewHolder).OnAttachedToWindow();
}
}
public override void OnViewDetachedFromWindow(Java.Lang.Object holder)
{
base.OnViewDetachedFromWindow(holder);
if (holder is CarouselViewHolder)
{
(holder as CarouselViewHolder).PrepareForReuse();
(holder as CarouselViewHolder).OnDetachedFromWindow();
}
}
public override void OnViewRecycled(Java.Lang.Object holder)
{
base.OnViewRecycled(holder);
if (holder is CarouselViewHolder)
{
(holder as CarouselViewHolder).PrepareForReuse();
}
}
}
With MvvmCross you shouldn't have to worry about writing your own ViewHolder or Adapter classes. The MvxRecyclerView takes care of implementing these classes and binds all of the ViewModels.
Define an MvxRecyclerView in your parent view:
<MvxRecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
local:MvxBind="ItemClick TileClickedCommand; ItemsSource Tiles;"
local:MvxItemTemplate="#layout/tileitemview" />
Note the MvxItemTemplate, this is where you specify a secondary view to use for each of your TileViewModels.
Your child view can look as simple as something like this:
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Format('{0:f}', Progress)" />
Note: I used a TextView to only represent how a child view will bind to a child ViewModel. The child view can contain any number of controls as you want.
I need to ignore a page while navigating back, I tried the following:
public override async Task OnNavigatingFromAsync(NavigatingEventArgs args)
{
if (args.NavigationMode == NavigationMode.Back)
{
args.Cancel = true;
NavigationService.Navigate(typeof(MainPage));
} else {
args.Cancel = false;
}
await Task.CompletedTask;
}
On the page that I am navigating away from, but it seems to only cancel the navigation.
What is the best way to tackle this issue?
In the page that you want to ignore, you can add the following method. This will allow to remove the current page from the navigation history.
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
// here NavigationService is supposed to be an instance of Template10 INavigationService
var backStack = NavigationService.FrameFacade.BackStack;
if (backStack.Count > 0 && backStack.Last().SourcePageType == this.GetType())
{
backStack.Remove(backStack.Last());
}
base.OnNavigatedFrom(e);
}
I'm used to WinForms, so this isn't behaving quite how I'd expect. In the code below, ActiveView is a Frame, Register is Page. I want to load the register page into the ActiveView and then change the text on a button. Even though the page loads, the debugger says that ActiveView.Content == null in SetCloseButtonText. Why is that?
private void btnRegister_Click(object sender, RoutedEventArgs e)
{
SwapActiveView(Register);
}
public void SwapActiveView(Page NewPage)
{
if (ActiveView.Content == null || !ActiveView.Content.Equals(NewPage))
{
if (ActiveView.Content != null)
{
PreviousViews.Add((Page)ActiveView.Content);
}
ActiveView.Content = NewPage;
}
else
{
ActiveView.Content = NewPage;
}
SetCloseButtonText();
}
private void SetCloseButtonText()
{
if (PreviousViews.Count == 0 && ActiveView.Content == null)
{
tbCloseButton.Text = "Close";
}
else
{
tbCloseButton.Text = "Back";
}
}
I ended up finding the answer. A frame navigates asynchronously, whether you call the navigate function or just change the content. So I simply needed to add the method and call my function then.
private void ActiveView_Navigated(object sender, NavigationEventArgs e)
{
SetCloseButtonText();
}
I have a strange recurring problem. Sometimes it goes away, other times it comes back. I can't pinpoint at all the issue, all my breakpoints seem to be hit in expected order.
When I navigate to a new page, my backstack keeps getting deleted, so pressing back just backgrounds the app. Obviously this is a problem.
I think it may be a result of my more complex page and viewmodel structures. I created a new class for all the NavigationHelper stuff for Pages enforcing that all my Pages subclass from the new class. I enforce that all my Pages attach themselves to a base PageViewModel class to resolve the communication between the two (I had a better way but Xaml doesn't play well), and I navigate using a NavigationService, where I call CurrentFrame, which is a static method for return Windows.Current.Content as Frame.
Here are what I think are relevant code. Any ideas? Thanks a bunch in advance. I have no clue what's going on :/
I navigate forward using the Navigate method in NavigationService (not the other two lolol), but my back button doesn't go back properly.
public abstract class BaseViewModelPage : Page
{
protected readonly NavigationHelper NavigationHelper;
protected BaseViewModelPage()
{
NavigationHelper = new NavigationHelper(this);
NavigationHelper.LoadState += navigationHelper_LoadState;
NavigationHelper.SaveState += navigationHelper_SaveState;
this.NavigationCacheMode = NavigationCacheMode.Required;
}
protected BasePageViewModel CurrentPageViewModel
{
get { return DataContext as BasePageViewModel; }
}
#region Navigation Registration
protected override void OnNavigatedTo(NavigationEventArgs e)
{
NavigationHelper.OnNavigatedTo(e);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
NavigationHelper.OnNavigatedFrom(e);
}
protected virtual void LoadState(LoadStateEventArgs e)
{
if (CurrentPageViewModel != null)
{
CurrentPageViewModel.LoadState(e);
}
}
protected virtual void SaveState(SaveStateEventArgs e)
{
if (CurrentPageViewModel != null)
{
CurrentPageViewModel.SaveState(e);
}
}
private void navigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
LoadState(e);
}
private void navigationHelper_SaveState(object sender, SaveStateEventArgs e)
{
SaveState(e);
}
#endregion
}
public abstract class BasePageViewModel : ViewModelBase
{
private bool _isLoading = false;
public bool IsLoading
{
get
{
return _isLoading;
}
set
{
if (_isLoading == value)
{
return;
}
_isLoading = value;
RaisePropertyChanged();
}
}
public virtual void LoadState(LoadStateEventArgs e)
{
}
public virtual void SaveState(SaveStateEventArgs e)
{
}
}
public class NavigationService : INavigationService
{
public static readonly Dictionary<Type, Type> PageDictionary;
static NavigationService()
{
PageDictionary = new Dictionary<Type, Type>();
PageDictionary.Add(typeof(LogInPageViewModel), typeof(LogInPage));
PageDictionary.Add(typeof(RegisterUserPageViewModel), typeof(RegisterUserPage));
}
public bool Navigate(Type pageViewModelType, Object parameter = null)
{
if (PageDictionary.ContainsKey(pageViewModelType))
{
if (parameter != null)
{
return App.CurrentFrame.Navigate(PageDictionary[pageViewModelType], parameter);
}
else
{
return App.CurrentFrame.Navigate(PageDictionary[pageViewModelType]);
}
}
return false;
}
public bool GoBack()
{
if (CanGoBack())
{
App.CurrentFrame.GoBack();
}
return false;
}
public bool CanGoBack()
{
return App.CurrentFrame.CanGoBack;
}
public bool NavigateAndRemoveSelf(Type pageViewModelType, object parameter = null)
{
if (Navigate(pageViewModelType, parameter))
{
if (App.CurrentFrame.CanGoBack)
{
App.CurrentFrame.BackStack.RemoveAt(App.CurrentFrame.BackStackDepth - 1);
return true;
}
}
return false;
}
public bool NavigateAndRemoveAll(Type pageViewModelType, object parameter = null)
{
if (Navigate(pageViewModelType, parameter))
{
while (App.CurrentFrame.CanGoBack)
{
App.CurrentFrame.BackStack.RemoveAt(App.CurrentFrame.BackStackDepth - 1);
}
return true;
}
return false;
}
}
Update [solved]:
The error is caused by using a Universal App Class Library.
I wanted to separate the NavigationHelper.cs class (generated by default in WP8 apps) into a library. so that I could unit test the VM directly (I could not reference the WP8 app with the Unit Test project). Thus, I placed the NavigationHelper.cs class, plus all my relevant code above, in a new Universal App Class Library.
The NavigationHelper class relies on two things, a WINDOWS_PHONE_APP macro in the BUILD, which affects this specific part in the NavigationHelper class, the HardwareButton BackPressed listener.
#if WINDOWS_PHONE_APP
Windows.Phone.UI.Input.HardwareButtons.BackPressed += HardwareButtons_BackPressed;
#else
and a second reliance on the Windows.Phone assembly. The assembly exists in a WP8 app, but not for a Universal App Class Library. This means that even if I add the WINDOWS_PHONE_APP macro to the library, the app will not compile. You cannot use the NavigationHelper generated by Windows Phone 8/8.1 projects inside a Universal App Class Library. I will try to raise this issue. Thanks!
Update [solved]:
The error is caused by using a Universal App Class Library.
I wanted to separate the NavigationHelper.cs class (generated by default in WP8 apps) into a library. so that I could unit test the VM directly (I could not reference the WP8 app with the Unit Test project). Thus, I placed the NavigationHelper.cs class, plus all my relevant code above, in a new Universal App Class Library.
The NavigationHelper class relies on two things, a WINDOWS_PHONE_APP macro in the BUILD, which affects this specific part in the NavigationHelper class, the HardwareButton BackPressed listener.
#if WINDOWS_PHONE_APP
Windows.Phone.UI.Input.HardwareButtons.BackPressed += HardwareButtons_BackPressed;
#else
...
#endif
Because the MACRO wasn't defined, the back button wouldn't actually go back.
A second problem was the missing Windows.Phone assembly. The assembly exists in a WP8 app, but not for a Universal App Class Library. This means that even if I add a WINDOWS_PHONE_APP macro to the library, the app will not compile. You cannot use the NavigationHelper generated by Windows Phone 8/8.1 projects inside a Universal App Class Library. I will try to raise this issue. Thanks!
You can leave your NavigationHelper in your shared project, just add this to your MainPage in the Windows Phone project..
static MainPage()
{
HardwareButtons.BackPressed += (sender, args) =>
{
var frame = Window.Current.Content as Frame;
if (frame != null && frame.CanGoBack)
{
frame.GoBack();
args.Handled = true;
}
};
}
This solved my BackButton issues.
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);
}
}