0

I added the IDisposable interface to one of my DatatTemplates and implemented the according methods. In the source code of a Plugin Dispose() is called on the View property of a UIViewController subclass. But the Dispose() method of my DataTemplate is never called. I can't step into the dispose call, the debugger only moves to the next line.

If I print out the type with View.GetType() I get the following result

Xamarin.Forms.Platform.iOS.Platform+DefaultRenderer

If I look into the View property (mouse over), there is a child property named Element, which corresponds to my type of DataTemplate. Isn't Dispose() be called on all "child" elements of a platform renderer? Why is this and how can I achieve that my custom Dispose() method of my DataTemplate is called?

Code example:

MainPage.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:local="clr-namespace:TestCarouselView"
             xmlns:controls="clr-namespace:CarouselView.FormsPlugin.Abstractions;assembly=CarouselView.FormsPlugin.Abstractions"
             x:Class="TestCarouselView.MainPage">

    <controls:CarouselViewControl x:Name="carouselView" Orientation="Horizontal" InterPageSpacing="10" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand">

    </controls:CarouselViewControl>

</ContentPage>

MainPage.xaml.cs

public partial class MainPage : ContentPage
{
    private int maxValue = 3;
    private CustomViewModel model;
    private DataTemplate template;

    public MainPage()
    {
        InitializeComponent();

        this.model = new CustomViewModel(this.maxValue);
        this.template = new DataTemplate(CreateDataTemplateView);

        this.carouselView.ItemsSource = this.model.ObjectList;
        this.carouselView.ItemTemplate = this.template;
        this.carouselView.Position = this.maxValue;
        this.carouselView.PositionSelected += CarouselView_PositionSelected;
    }

    private void CarouselView_PositionSelected(object sender, CarouselView.FormsPlugin.Abstractions.PositionSelectedEventArgs e)
    {
        var currentObject = this.model.ObjectList.ElementAt(e.NewValue);
        var nextToLastObject = this.model.ObjectList.ElementAt(this.model.ObjectList.Count - 2);
        var secondObject = this.model.ObjectList.ElementAt(1);

        if (currentObject == nextToLastObject)
        {
            this.SwipeAppend();
        }
        else if (currentObject == secondObject)
        {
            this.SwipePrepend();
        }
    }

    private View CreateDataTemplateView()
    {
        CustomTemplateView template = new CustomTemplateView();
        template.BindingContext = this.model.ObjectList;
        // here I'm subscribing to events from the DataTemplate
        template.ElementSelected += OnElementSelected;

        return (View)template;
    }

    public void SwipeAppend()
    {
        int newId = Int32.Parse(this.model.ObjectList[this.model.ObjectList.Count - 1].Id) + 1;
        this.model.ObjectList.Add(new CustomObject(newId.ToString()));
        this.model.ObjectList.RemoveAt(0);
    }

    public void SwipePrepend()
    {
        int oldPosition = this.carouselView.Position;
        int oldId = Int32.Parse(this.model.ObjectList[0].Id);
        this.model.ObjectList.Insert(0, new CustomObject((oldId - 1).ToString()));
        this.model.ObjectList.RemoveAt(this.model.ObjectList.Count - 1);
        this.carouselView.Position = oldPosition + 1;
    }
}

CustomViewModel.cs

public class CustomViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableCollection<CustomObject> ObjectList { get; set; }

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public CustomViewModel(int amount)
    {
        ObjectList = new ObservableCollection<CustomObject>();

        for (int i = -amount; i <= amount; i++)
        {
            ObjectList.Add(new CustomObject(i.ToString()));
        }
    }
}

CustomObject.cs

public class CustomObject
{
    private string id;

    public string Id
    {
        get { return this.id; }
        set { this.id = value; }
    }

    public CustomObject(string id)
    {
        this.id = id;
    }
}

CustomTemplateView.xaml

<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="TestCarouselView.CustomTemplateView">
    <ContentView.Content>
        <skia:SKCanvasView
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0,0,1,1"
            x:Name="drawingArea"
            IgnorePixelScaling="True" />
    </ContentView.Content>
</ContentView>

CustomTemplateView.xaml.cs

public partial class CustomTemplateView : ContentView, IDisposable
{
    private CustomObject myData;
    public CustomTemplateView()
    {
        InitializeComponent();

        // here I'm subscribing to MessagingCenter
        MessagingCenter.Subscribe<SelectElement>(this, Msg_SelectElement, message =>
        {
            this.SelectElement(message.Element);
        });
    }

    protected override void OnBindingContextChanged()
    {
        base.OnBindingContextChanged();

        this.myData = this.BindingContext as CustomObject;
        if (this.myData != null)
        {
            this.SetupContent();
        }
    }

    private void SetupContent()
    {
        this.drawingArea.PaintSurface += drawingArea_PaintSurface;
    }

    private void drawingArea_PaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        args.Surface.Canvas.Clear();

        SKPaint textPaint = new SKPaint
        {
            IsAntialias = true,
            Style = SKPaintStyle.Fill,
            Color = SKColors.Black,
            TextSize = 92,
            StrokeWidth = 1,
        };
        args.Surface.Canvas.DrawText(this.myData.Id, args.Info.Width/2, args.Info.Height/2, textPaint);
    }

    // Implement IDisposable. 
    // Do not make this method virtual. 
    // A derived class should not be able to override this method. 
    public void Dispose()
    {
        Dispose(true);
        // This object will be cleaned up by the Dispose method. 
        // Therefore, you should call GC.SupressFinalize to 
        // take this object off the finalization queue 
        // and prevent finalization code for this object 
        // from executing a second time.
        GC.SuppressFinalize(this);
    }

    // Dispose(bool disposing) executes in two distinct scenarios. 
    // If disposing equals true, the method has been called directly 
    // or indirectly by a user's code. Managed and unmanaged resources 
    // can be disposed. 
    // If disposing equals false, the method has been called by the 
    // runtime from inside the finalizer and you should not reference 
    // other objects. Only unmanaged resources can be disposed. 
    protected virtual void Dispose(bool disposing)
    {
        System.Diagnostics.Debug.WriteLine("Dispose is called");

        // Check to see if Dispose has already been called. 
        if (!this.disposed)
        {
            // If disposing equals true, dispose all managed 
            // and unmanaged resources. 
            if (disposing)
            {
                // Dispose managed resources.
                this.drawingArea.PaintSurface -= drawingArea_PaintSurface;
                MessagingCenter.Unsubscribe<SelectElement>(this, Msg_SelectElement);
            }

            // Call the appropriate methods to clean up 
            // unmanaged resources here. 
            // If disposing is false, 
            // only the following code is executed.
            // ...

            // Note disposing has been done.
            disposed = true;
        }
    }

    // Use C# destructor syntax for finalization code. 
    // This destructor will run only if the Dispose method 
    // does not get called. 
    // It gives your base class the opportunity to finalize. 
    // Do not provide destructors in types derived from this class.
    ~CustomTemplateView()
    {
        System.Diagnostics.Debug.WriteLine("Finalizer is called");
        // Do not re-create Dispose clean-up code here. 
        // Calling Dispose(false) is optimal in terms of 
        // readability and maintainability.
        Dispose(false);
    }
}

Modifications at the CarouselView plugin:

CarouselViewImplementation.cs

async Task RemovePage(int position)
{
    if (Element == null || pageController == null || Source == null) return;

    if (Source?.Count > 0)
    {
        // To remove latest page, rebuild pageController or the page wont disappear
        if (Source.Count == 1)
        {
            Source.RemoveAt(position);
            SetNativeView();
        }
        else
        {
            // ### Beginn of modification ###
            // Remove controller from ChildViewControllers
            if (ChildViewControllers != null)
            {
                ViewContainer child = ChildViewControllers.FirstOrDefault(c => c.Tag == Source[position]);
                if (child != null)
                {
                    ChildViewControllers.Remove(child);
                    child.Dispose();
                }
            }

            object elementToRemove = Source.ElementAt(position);
            //Source.RemoveAt(position);
            Source.Remove(elementToRemove);
            //elementToRemove.Dispose();

            // ### End of modification ###

            // To remove current page
            if (position == Element.Position)
            {
                var newPos = position - 1;
                if (newPos == -1)
                    newPos = 0;

                // With a swipe transition
                if (Element.AnimateTransition)
                    await Task.Delay(100);

                var direction = position == 0 ? UIPageViewControllerNavigationDirection.Forward : UIPageViewControllerNavigationDirection.Reverse;
                var firstViewController = CreateViewController(newPos);

                pageController.SetViewControllers(new[] { firstViewController }, direction, Element.AnimateTransition, s =>
                {
                    isChangingPosition = true;
                    Element.Position = newPos;
                    isChangingPosition = false;

                    SetArrowsVisibility();
                    SetIndicatorsCurrentPage();

                    // Invoke PositionSelected as DidFinishAnimating is only called when touch to swipe
                    Element.SendPositionSelected();
                    Element.PositionSelectedCommand?.Execute(null);
                });
            }
            else
            {
                var firstViewController = pageController.ViewControllers[0];

                pageController.SetViewControllers(new[] { firstViewController }, UIPageViewControllerNavigationDirection.Forward, false, s =>
                {
                    SetArrowsVisibility();
                    SetIndicatorsCurrentPage();

                    // Invoke PositionSelected as DidFinishAnimating is only called when touch to swipe
                    Element.SendPositionSelected();
                    Element.PositionSelectedCommand?.Execute(null);
                });
            }
        }

        prevPosition = Element.Position;
    }
}

UIViewController CreateViewController(int index)
{
    // ### Beginn of modification ###
    if (ChildViewControllers == null)
        ChildViewControllers = new List<ViewContainer>();
    // ### End of modification ###

    // Significant Memory Leak for iOS when using custom layout for page content #125
    var newTag = Source[index];
    foreach (ViewContainer child in pageController.ChildViewControllers)
    {
        if (child.Tag == newTag)
            return child;
    }

    View formsView = null;

    object bindingContext = null;

    if (Source != null && Source?.Count > 0)
        bindingContext = Source.Cast<object>().ElementAt(index);

    var dt = bindingContext as DataTemplate;
    // Support for List<View> as ItemsSource
    var view = bindingContext as View;

    // Support for List<DataTemplate> as ItemsSource
    if (dt != null)
    {
        formsView = (View)dt.CreateContent();
    }
    else
    {
        if (view != null)
        {
            if (ChildViewControllers == null)
                ChildViewControllers = new List<ViewContainer>();

            // Return from the local copy of controllers
            foreach(ViewContainer controller in ChildViewControllers)
            {
                if (controller.Tag == view)
                {
                    return controller;
                }
            }

            formsView = view;
        }
        else
        {
            var selector = Element.ItemTemplate as DataTemplateSelector;
            if (selector != null)
                formsView = (View)selector.SelectTemplate(bindingContext, Element).CreateContent();
            else
                formsView = (View)Element.ItemTemplate.CreateContent();

            formsView.BindingContext = bindingContext;
        }
    }

    // HeightRequest fix
    formsView.Parent = this.Element;

    // UIScreen.MainScreen.Bounds.Width, UIScreen.MainScreen.Bounds.Height
    var rect = new CGRect(Element.X, Element.Y, ElementWidth, ElementHeight);
    var nativeConverted = formsView.ToiOS(rect);

    //if (dt == null && view == null)
        //formsView.Parent = null;

    var viewController = new ViewContainer();
    viewController.Tag = bindingContext;
    viewController.View = nativeConverted;

    // Only happens when ItemsSource is List<View>
    if (ChildViewControllers != null)
    {
        ChildViewControllers.Add(viewController);
        Console.WriteLine("ChildViewControllers count = " + ChildViewControllers.Count());
    }

    return viewController;
}
testing
  • 19,681
  • 50
  • 236
  • 417
  • 1. Question is why do you need Dispose implementation? Are you using some native resources in your class? 2. You don't need to force call child Dispose method, when you Dispose parent, garbage collector will clear everything at some point and to handle something at this point you want to use [destructor](https://www.geeksforgeeks.org/destructors-in-c-sharp/). – Renatas M. Oct 02 '19 at 07:37
  • 1
    Looking at the Xamarin Forms code on Github, while `Xamarin.Forms.Plaform.iOS.PlatformRenderer` has a `Dispose` method, it doesn't appear to dispose of any of its children, nor does its parent `UIKit.UIViewController` from the xamarin-macios repository. – Powerlord Oct 02 '19 at 07:42
  • @Reniuz: Regarding 1:I have the issue, that some memory is never released - which leads to an app crash. My current assumption is that it has to do with how the plugin works. I'm subscribing to events, using the messaging center and so on, but I never unsubsribe from it. That's the reason why I do it in the dispose implementation. Regarding 2: The garbage collector can't clear it up and I don't know exactly why. I use the general available [disposable example implementation](https://docs.microsoft.com/en-us/dotnet/api/system.idisposable.dispose?view=netframework-4.8), which use the destructor. – testing Oct 02 '19 at 07:46
  • @Powerlord: So there is no possibility to clean up my `DataTemplate`? Other than not using event subscribing or messaging center? – testing Oct 02 '19 at 07:50
  • @testing I'm not familiar enough with Xamarin.Forms to answer that question. – Powerlord Oct 02 '19 at 07:54
  • Could you add a minimal code example to illustrate how your code works? – Renatas M. Oct 02 '19 at 07:56
  • @Reniuz: I added the example. Hope everything you need is in. It can't be shorter ... – testing Oct 02 '19 at 08:33
  • `IDisposable` is not a destructor, and should only be implemented in the following circumstances: **1.** When the class owns unmanaged resources. Typical unmanaged resources that require releasing include files, streams, and network connections. **2.** When the class owns managed IDisposable resources. – Lucas Zhang Oct 02 '19 at 11:15
  • @LucasZhang-MSFT: According to the memory management guidelines one should also unsubscribe from events and so on to prevent memory leaks/cycles. The view doesn't has a `OnAppearing()` or `OnDisappearing()` method and I also can't manage the lifecycle (because the plugin does it for me), so that a cleanup method would help. Where should I unsubscribe then, when not in `Dispose()`? The `DataTemplate` is making things also more complicated ... – testing Oct 02 '19 at 11:55
  • 1
    You can add lifecycle to View .Check https://www.pshul.com/2018/03/27/xamarin-forms-add-views-lifecycle-events/ – Lucas Zhang Oct 11 '19 at 08:36

0 Answers0