Change SourceChanged event on ImageSource to weak event to allow Images (#268)
authorE.Z. Hart <hartez@users.noreply.github.com>
Tue, 2 Aug 2016 20:54:57 +0000 (14:54 -0600)
committerJason Smith <jason.smith@xamarin.com>
Tue, 2 Aug 2016 20:54:57 +0000 (13:54 -0700)
referencing application-wide StaticResource ImageSources to be GCed

Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla42069.cs [new file with mode: 0644]
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla42069_Page.xaml [new file with mode: 0644]
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla42069_Page.xaml.cs [new file with mode: 0644]
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
Xamarin.Forms.Core/Image.cs
Xamarin.Forms.Core/ImageSource.cs
Xamarin.Forms.Core/WeakEventManager.cs [new file with mode: 0644]
Xamarin.Forms.Core/Xamarin.Forms.Core.csproj
Xamarin.Forms.Platform.WinRT/WindowsDeviceInfo.cs

diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla42069.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla42069.cs
new file mode 100644 (file)
index 0000000..7abfa5d
--- /dev/null
@@ -0,0 +1,74 @@
+using System;
+using Xamarin.Forms.Controls.Issues;
+using Xamarin.Forms.CustomAttributes;
+using Xamarin.Forms.Internals;
+
+namespace Xamarin.Forms.Controls
+{
+       [Preserve(AllMembers = true)]
+       [Issue(IssueTracker.Bugzilla, 42069, "Garbage Collector can not collect pages that use ImageSource as a StaticResource",
+               PlatformAffected.All)]
+       public class Bugzilla42069 : TestNavigationPage
+       {
+               protected override void Init()
+               {
+                       if (Application.Current.Resources == null)
+                       {
+                               Application.Current.Resources = new ResourceDictionary();
+                       }
+
+                       if (!Application.Current.Resources.ContainsKey("SomeSmallImage"))
+                       {
+                               var smallImage = new OnPlatform<ImageSource> {
+                                       Android = ImageSource.FromFile("coffee.png"),
+                                       WinPhone = ImageSource.FromFile("bank.png"),
+                                       iOS = ImageSource.FromFile("coffee.png")
+                               };
+
+                               Application.Current.Resources.Add("SomeSmallImage", smallImage);
+                       }
+
+                       const string instructions1 = @"Tap the Start button and follow the instructions on the next page.";
+                       string instructions2 =
+                               $"When you return to this page, tap the Collect button. The message \n'{Bugzilla42069_Page.DestructorMessage}'\n should appear at least once in the debug output.";
+
+                       var label1 = new Label { Text = instructions1 };
+                       var label2 = new Label { Text = instructions2, HorizontalTextAlignment = TextAlignment.Center };
+
+                       var startButton = new Button { Text = "Start" };
+                       startButton.Clicked += (sender, args) =>
+                       {
+                               // We have to do the push-pop-push dance because NavigationPage
+                               // holds a reference to its last page for unrelated reasons; our concern 
+                               // here is that the first Bugzilla42069_Page that we pushed gets collected
+                               PushAsync(new Bugzilla42069_Page(), false);
+                               PopAsync(false);
+                               PushAsync(new Bugzilla42069_Page(), false);
+                       };
+
+                       var collectButton = new Button { Text = "Collect" };
+                       collectButton.Clicked += (sender, args) =>
+                       {
+                               GC.Collect();
+                               GC.Collect();
+                               GC.Collect();
+                       };
+
+                       var startPage = new ContentPage
+                       {
+                               Content = new StackLayout
+                               {
+                                       Children =
+                                       {
+                                               label1,
+                                               startButton,
+                                               label2,
+                                               collectButton
+                                       }
+                               }
+                       };
+
+                       PushAsync(startPage);
+               }
+       }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla42069_Page.xaml b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla42069_Page.xaml
new file mode 100644 (file)
index 0000000..5f98e19
--- /dev/null
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+             x:Class="Xamarin.Forms.Controls.Issues.Bugzilla42069_Page">
+       <StackLayout>
+               <Image x:Name="ChangingImage" />
+               <Button x:Name="Button2" Text="Change Image"></Button>
+               <Label Text="Tap the 'Change Image' button; the image at the top should toggle between two images. If the image doesn't toggle, the test has failed." ></Label>
+               <Label Text="There should be several instances of the same image below." ></Label>
+               <Button x:Name="Button" Text="Back"></Button>
+               <Label Text="Tap the 'Back' button." ></Label>
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage} "/>
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+               <Image Source="{StaticResource SomeSmallImage}" />
+       </StackLayout>
+</ContentPage>
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla42069_Page.xaml.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla42069_Page.xaml.cs
new file mode 100644 (file)
index 0000000..2031e56
--- /dev/null
@@ -0,0 +1,36 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+
+namespace Xamarin.Forms.Controls.Issues
+{
+       public partial class Bugzilla42069_Page : ContentPage
+       {
+               public const string DestructorMessage = ">>>>>>>>>> Bugzilla42069_Page destructor <<<<<<<<<<";
+
+               public Bugzilla42069_Page()
+               {
+                       InitializeComponent();
+
+                       ImageWhichChanges = ImageSource.FromFile("oasissmall.jpg") as FileImageSource;
+
+                       ChangingImage.SetBinding(Image.SourceProperty, nameof(ImageWhichChanges));
+
+                       Button.Clicked += (sender, args) => Navigation.PopAsync(false);
+
+                       Button2.Clicked += (sender, args) =>
+                       {
+                               ImageWhichChanges.File = ImageWhichChanges.File == "bank.png" ? "oasissmall.jpg" : "bank.png";
+                       };
+
+                       BindingContext = this;
+               }
+
+               ~Bugzilla42069_Page()
+               {
+                       Debug.WriteLine(DestructorMessage);
+               }
+
+               public FileImageSource ImageWhichChanges { get; set; }
+       }
+}
\ No newline at end of file
index 6531b47..a98b97d 100644 (file)
     <Compile Include="$(MSBuildThisFileDirectory)Bugzilla40998.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Bugzilla41205.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Bugzilla41424.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)Bugzilla42069.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)Bugzilla42069_Page.xaml.cs">
+      <DependentUpon>Bugzilla42069_Page.xaml</DependentUpon>
+      <SubType>Code</SubType>
+    </Compile>
     <Compile Include="$(MSBuildThisFileDirectory)Bugzilla42074.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Bugzilla42075.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)CarouselAsync.cs" />
       <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
     </EmbeddedResource>
   </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="$(MSBuildThisFileDirectory)Bugzilla42069_Page.xaml">
+      <SubType>Designer</SubType>
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+  </ItemGroup>
 </Project>
\ No newline at end of file
index 4292f95..dd2692a 100644 (file)
@@ -1,4 +1,6 @@
 using System;
+using System.Collections.Generic;
+using System.Reflection;
 using Xamarin.Forms.Internals;
 using Xamarin.Forms.Platform;
 
@@ -7,8 +9,8 @@ namespace Xamarin.Forms
        [RenderWith(typeof(_ImageRenderer))]
        public class Image : View, IImageController
        {
-               public static readonly BindableProperty SourceProperty = BindableProperty.Create("Source", typeof(ImageSource), typeof(Image), default(ImageSource), propertyChanging: OnSourcePropertyChanging,
-                       propertyChanged: OnSourcePropertyChanged);
+               public static readonly BindableProperty SourceProperty = BindableProperty.Create("Source", typeof(ImageSource), typeof(Image), default(ImageSource), 
+                       propertyChanging: OnSourcePropertyChanging, propertyChanged: OnSourcePropertyChanged);
 
                public static readonly BindableProperty AspectProperty = BindableProperty.Create("Aspect", typeof(Aspect), typeof(Image), Aspect.AspectFit);
 
@@ -126,6 +128,7 @@ namespace Xamarin.Forms
                                newvalue.SourceChanged += OnSourceChanged;
                                SetInheritedBindingContext(newvalue, BindingContext);
                        }
+
                        InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
                }
 
@@ -138,7 +141,7 @@ namespace Xamarin.Forms
                {
                        if (oldvalue == null)
                                return;
-
+                       
                        oldvalue.SourceChanged -= OnSourceChanged;
                        try
                        {
index f65d061..3b59f7f 100644 (file)
@@ -14,6 +14,8 @@ namespace Xamarin.Forms
 
                TaskCompletionSource<bool> _completionSource;
 
+               readonly WeakEventManager _weakEventManager = new WeakEventManager();
+
                protected ImageSource()
                {
                }
@@ -132,11 +134,13 @@ namespace Xamarin.Forms
 
                protected void OnSourceChanged()
                {
-                       EventHandler eh = SourceChanged;
-                       if (eh != null)
-                               eh(this, EventArgs.Empty);
+                       _weakEventManager.HandleEvent(this, EventArgs.Empty, nameof(SourceChanged));
                }
 
-               internal event EventHandler SourceChanged;
+               internal event EventHandler SourceChanged
+               {
+                       add { _weakEventManager.AddEventHandler(nameof(SourceChanged), value); }
+                       remove { _weakEventManager.RemoveEventHandler(nameof(SourceChanged), value);}
+               }
        }
 }
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/WeakEventManager.cs b/Xamarin.Forms.Core/WeakEventManager.cs
new file mode 100644 (file)
index 0000000..226f329
--- /dev/null
@@ -0,0 +1,159 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace Xamarin.Forms
+{
+       internal class WeakEventManager
+       {
+               readonly Dictionary<string, List<Subscription>> _eventHandlers =
+                       new Dictionary<string, List<Subscription>>();
+
+               public void AddEventHandler<TEventArgs>(string eventName, EventHandler<TEventArgs> handler)
+                       where TEventArgs : EventArgs
+               {
+                       if (eventName == null)
+                       {
+                               throw new ArgumentNullException(nameof(eventName));
+                       }
+
+                       if (handler == null)
+                       {
+                               throw new ArgumentNullException(nameof(handler));
+                       }
+
+                       AddEventHandler(eventName, handler.Target, handler.GetMethodInfo());
+               }
+
+               public void AddEventHandler(string eventName, EventHandler handler)
+               {
+                       if (eventName == null)
+                       {
+                               throw new ArgumentNullException(nameof(eventName));
+                       }
+
+                       if (handler == null)
+                       {
+                               throw new ArgumentNullException(nameof(handler));
+                       }
+
+                       AddEventHandler(eventName, handler.Target, handler.GetMethodInfo());
+               }
+
+               public void HandleEvent(object sender, object args, string eventName)
+               {
+                       var toRaise = new List<Tuple<object, MethodInfo>>();
+
+                       List<Subscription> target;
+                       if (_eventHandlers.TryGetValue(eventName, out target))
+                       {
+                               foreach (Subscription subscription in target)
+                               {
+                                       object subscriber = subscription.Subscriber.Target;
+
+                                       if (subscriber == null)
+                                       {
+                                               // The subscriber was collected, so there's no need to keep this subscription around
+                                               target.Remove(subscription);
+                                       }
+                                       else
+                                       {
+                                               toRaise.Add(Tuple.Create(subscriber, subscription.Handler));
+                                       }
+                               }
+                       }
+
+                       foreach (Tuple<object, MethodInfo> tuple in toRaise)
+                       {
+                               tuple.Item2.Invoke(tuple.Item1, new[] { sender, args });
+                       }
+               }
+
+               public void RemoveEventHandler<TEventArgs>(string eventName, EventHandler<TEventArgs> handler)
+                       where TEventArgs : EventArgs
+               {
+                       if (eventName == null)
+                       {
+                               throw new ArgumentNullException(nameof(eventName));
+                       }
+
+                       if (handler == null)
+                       {
+                               throw new ArgumentNullException(nameof(handler));
+                       }
+
+                       RemoveEventHandler(eventName, handler.Target, handler.GetMethodInfo());
+               }
+
+               public void RemoveEventHandler(string eventName, EventHandler handler)
+               {
+                       if (eventName == null)
+                       {
+                               throw new ArgumentNullException(nameof(eventName));
+                       }
+
+                       if (handler == null)
+                       {
+                               throw new ArgumentNullException(nameof(handler));
+                       }
+
+                       RemoveEventHandler(eventName, handler.Target, handler.GetMethodInfo());
+               }
+
+               void AddEventHandler(string eventName, object handlerTarget, MethodInfo methodInfo)
+               {
+                       List<Subscription> target;
+                       if (!_eventHandlers.TryGetValue(eventName, out target))
+                       {
+                               target = new List<Subscription>();
+                               _eventHandlers.Add(eventName, target);
+                       }
+
+                       target.Add(new Subscription(new WeakReference(handlerTarget), methodInfo));
+               }
+
+               void RemoveEventHandler(string eventName, object handlerTarget, MemberInfo methodInfo)
+               {
+                       List<Subscription> subscriptions;
+                       if (!_eventHandlers.TryGetValue(eventName, out subscriptions))
+                       {
+                               return;
+                       }
+
+                       for (int n = subscriptions.Count; n > 0; n--)
+                       {
+                               Subscription current = subscriptions[n - 1];
+
+                               if (current.Subscriber != handlerTarget
+                                   || current.Handler.Name != methodInfo.Name)
+                               {
+                                       continue;
+                               }
+
+                               subscriptions.Remove(current);
+                       }
+               }
+
+               struct Subscription
+               {
+                       public Subscription(WeakReference subscriber, MethodInfo handler)
+                       {
+                               if (subscriber == null)
+                               {
+                                       throw new ArgumentNullException(nameof(subscriber));
+                               }
+
+                               if (handler == null)
+                               {
+                                       throw new ArgumentNullException(nameof(handler));
+                               }
+
+                               Subscriber = subscriber;
+                               Handler = handler;
+                       }
+
+                       public readonly WeakReference Subscriber;
+                       public readonly MethodInfo Handler;
+               }
+       }
+}
\ No newline at end of file
index d0cb293..0541673 100644 (file)
     <Compile Include="Internals\ToolbarTracker.cs" />
     <Compile Include="ViewExtensions.cs" />
     <Compile Include="ViewState.cs" />
+    <Compile Include="WeakEventManager.cs" />
     <Compile Include="WeakReferenceExtensions.cs" />
     <Compile Include="WebNavigatedEventArgs.cs" />
     <Compile Include="WebNavigatingEventArgs.cs" />
index f3d6d10..235fe2d 100644 (file)
@@ -77,9 +77,13 @@ namespace Xamarin.Forms.Platform.WinRT
                        if (_isDisposed)
                                return;
 
+                       if (disposing)
+                       {
+                               _information.OrientationChanged -= OnOrientationChanged;
+                               _information = null;
+                       }
+
                        _isDisposed = true;
-                       _information.OrientationChanged -= OnOrientationChanged;
-                       _information = null;
 
                        base.Dispose(disposing);
                }