From 9ba9414c223216e0d36ceef25ae34c12e252bb81 Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Thu, 27 Jul 2023 03:24:57 +0300 Subject: [PATCH 01/12] unit test for issue added --- .../HotKeyedControlsTests.cs | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs diff --git a/tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs b/tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs new file mode 100644 index 0000000000..55a3f0d5d4 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs @@ -0,0 +1,122 @@ +using System; +using System.Windows.Input; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.LogicalTree; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + internal class HotKeyedTextBox : TextBox, ICommandSource + { + private class DelegateCommand : ICommand + { + private readonly Action _action; + public DelegateCommand(Action action) => _action = action; + public event EventHandler CanExecuteChanged { add { } remove { } } + public bool CanExecute(object parameter) => true; + public void Execute(object parameter) => _action(); + } + + public static readonly StyledProperty HotKeyProperty = + HotKeyManager.HotKeyProperty.AddOwner(); + + private KeyGesture _hotkey; + + public KeyGesture HotKey + { + get => GetValue(HotKeyProperty); + set => SetValue(HotKeyProperty, value); + } + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (_hotkey != null) + { + this.SetValue(HotKeyProperty, _hotkey); + } + + base.OnAttachedToLogicalTree(e); + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (this.HotKey != null) + { + _hotkey = this.HotKey; + this.SetValue(HotKeyProperty, null); + } + + base.OnDetachedFromLogicalTree(e); + } + + public void CanExecuteChanged(object sender, EventArgs e) + { + } + + protected override Type StyleKeyOverride => typeof(TextBox); + + public ICommand Command => _command; + + public object CommandParameter => null; + + private readonly DelegateCommand _command; + + public HotKeyedTextBox() + { + _command = new DelegateCommand(() => Focus()); + } + } + + public class HotKeyedControlsTests + { + private static Window PreparedWindow(object content = null) + { + var platform = AvaloniaLocator.Current.GetRequiredService(); + var windowImpl = Mock.Get(platform.CreateWindow()); + windowImpl.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor()); + var w = new Window(windowImpl.Object) { Content = content }; + w.ApplyTemplate(); + return w; + } + + private static IDisposable CreateServicesWithFocus() + { + return UnitTestApplication.Start( + TestServices.StyledWindow.With( + windowingPlatform: new MockWindowingPlatform( + null, + window => MockWindowingPlatform.CreatePopupMock(window).Object), + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice())); + } + + [Fact] + public void HotKeyedTextBox_Focus_Performed_On_Hotkey() + { + using var _ = CreateServicesWithFocus(); + + var keyboardDevice = new KeyboardDevice(); + var hotKeyedTextBox = new HotKeyedTextBox { HotKey = new KeyGesture(Key.F, KeyModifiers.Control) }; + var root = PreparedWindow(); + root.Content = hotKeyedTextBox; + root.Show(); + + Assert.False(hotKeyedTextBox.IsFocused); + + keyboardDevice.ProcessRawEvent( + new RawKeyEventArgs( + keyboardDevice, + 0, + root, + RawKeyEventType.KeyDown, + Key.F, + RawInputModifiers.Control)); + + Assert.True(hotKeyedTextBox.IsFocused); + } + } +} From a5c7b1d35842b40592bc04e5f0565243f1ce6e05 Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Thu, 27 Jul 2023 03:29:27 +0300 Subject: [PATCH 02/12] issue fix --- src/Avalonia.Controls/HotkeyManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index de753f0bd0..6ad4a8cc76 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -149,10 +149,10 @@ namespace Avalonia.Controls return; var control = args.Sender as Control; - if (control is not IClickableControl) + if (control is not IClickableControl and not ICommandSource) { Logging.Logger.TryGet(Logging.LogEventLevel.Warning, Logging.LogArea.Control)?.Log(control, - $"The element {args.Sender.GetType().Name} does not implement IClickableControl and does not support binding a HotKey ({args.NewValue})."); + $"The element {args.Sender.GetType().Name} does not implement IClickableControl nor ICommandSource and does not support binding a HotKey ({args.NewValue})."); return; } From d0b1389ee888d3ae2bac4a12dc04386d703e9062 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 2 Aug 2023 12:51:48 +0600 Subject: [PATCH 03/12] override_redirect and Handle fixes for X11Window --- src/Avalonia.X11/X11Window.cs | 36 +++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index be2d754819..a2019c276b 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -17,7 +17,6 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Platform.Storage; -using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.X11.Glx; @@ -36,6 +35,7 @@ namespace Avalonia.X11 { private readonly AvaloniaX11Platform _platform; private readonly bool _popup; + private readonly bool _overrideRedirect; private readonly X11Info _x11; private XConfigureEvent? _configure; private PixelPoint? _configurePoint; @@ -72,10 +72,11 @@ namespace Avalonia.X11 WaitPaint } - public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent) + public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, bool overrideRedirect = false) { _platform = platform; _popup = popupParent != null; + _overrideRedirect = _popup || overrideRedirect; _x11 = platform.Info; _mouse = new MouseDevice(); _touch = new TouchDevice(); @@ -92,7 +93,7 @@ namespace Avalonia.X11 | SetWindowValuemask.BackPixmap | SetWindowValuemask.BackingStore | SetWindowValuemask.BitGravity | SetWindowValuemask.WinGravity; - if (_popup) + if (_overrideRedirect) { attr.override_redirect = 1; valueMask |= SetWindowValuemask.OverrideRedirect; @@ -155,7 +156,7 @@ namespace Avalonia.X11 else _renderHandle = _handle; - Handle = new SurfacePlatformHandle(this); + Handle = new PlatformHandle(_handle, "XID"); _realSize = new PixelSize(defaultWidth, defaultHeight); platform.Windows[_handle] = OnEvent; XEventMask ignoredMask = XEventMask.SubstructureRedirectMask @@ -165,15 +166,18 @@ namespace Avalonia.X11 ignoredMask |= platform.XI2.AddWindow(_handle, this); var mask = new IntPtr(0xffffff ^ (int)ignoredMask); XSelectInput(_x11.Display, _handle, mask); - var protocols = new[] + if (!_overrideRedirect) { - _x11.Atoms.WM_DELETE_WINDOW - }; - XSetWMProtocols(_x11.Display, _handle, protocols, protocols.Length); - XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_WINDOW_TYPE, _x11.Atoms.XA_ATOM, - 32, PropertyMode.Replace, new[] {_x11.Atoms._NET_WM_WINDOW_TYPE_NORMAL}, 1); + var protocols = new[] + { + _x11.Atoms.WM_DELETE_WINDOW + }; + XSetWMProtocols(_x11.Display, _handle, protocols, protocols.Length); + XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_WINDOW_TYPE, _x11.Atoms.XA_ATOM, + 32, PropertyMode.Replace, new[] { _x11.Atoms._NET_WM_WINDOW_TYPE_NORMAL }, 1); - SetWmClass(_platform.Options.WmClass); + SetWmClass(_platform.Options.WmClass); + } var surfaces = new List { @@ -187,7 +191,7 @@ namespace Avalonia.X11 if (glx != null) surfaces.Insert(0, new GlxGlPlatformSurface(new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle))); - surfaces.Add(Handle); + surfaces.Add(new SurfacePlatformHandle(this)); Surfaces = surfaces.ToArray(); UpdateMotifHints(); @@ -257,6 +261,8 @@ namespace Avalonia.X11 private void UpdateMotifHints() { + if(_overrideRedirect) + return; var functions = MotifFunctions.Move | MotifFunctions.Close | MotifFunctions.Resize | MotifFunctions.Minimize | MotifFunctions.Maximize; var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border | @@ -286,6 +292,8 @@ namespace Avalonia.X11 private void UpdateSizeHints(PixelSize? preResize) { + if(_overrideRedirect) + return; var min = _minMaxSize.minSize; var max = _minMaxSize.maxSize; @@ -507,7 +515,7 @@ namespace Avalonia.X11 } UpdateImePosition(); - if (changedSize && !updatedSizeViaScaling && !_popup) + if (changedSize && !updatedSizeViaScaling && !_overrideRedirect) Resized?.Invoke(ClientSize, WindowResizeReason.Unspecified); }, DispatcherPriority.AsyncRenderTargetResize); @@ -984,7 +992,7 @@ namespace Avalonia.X11 XConfigureResizeWindow(_x11.Display, _renderHandle, pixelSize); XFlush(_x11.Display); - if (force || !_wasMappedAtLeastOnce || (_popup && needImmediatePopupResize)) + if (force || !_wasMappedAtLeastOnce || (_overrideRedirect && needImmediatePopupResize)) { _realSize = pixelSize; Resized?.Invoke(ClientSize, reason); From ec21caf7fe0419b2284e34d63cd13da6de9460dc Mon Sep 17 00:00:00 2001 From: 3dfxuser <3dfxuser@gmail.com> Date: Wed, 2 Aug 2023 11:43:53 +0300 Subject: [PATCH 04/12] Add null check for TextInputMethodClient in OnSelectionChanged() method --- src/Android/Avalonia.Android/AndroidInputMethod.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index 7d5130cf5d..f708d6936c 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -113,6 +113,11 @@ namespace Avalonia.Android private void OnSelectionChanged() { + if (Client is null) + { + return; + } + var selection = Client.Selection; _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); From f253b614b3dcf3c8acedf0614cbbfd6b6287c105 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 1 Aug 2023 18:12:53 +0200 Subject: [PATCH 05/12] Generate pdb for ref assemblies --- nukebuild/Build.cs | 5 +- nukebuild/RefAssemblyGenerator.cs | 109 ++++++++++++++++++------------ 2 files changed, 70 insertions(+), 44 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index bbfc28aa9f..f8fbf64e83 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -279,8 +279,9 @@ partial class Build : NukeBuild if(!Numerge.NugetPackageMerger.Merge(Parameters.NugetIntermediateRoot, Parameters.NugetRoot, config, new NumergeNukeLogger())) throw new Exception("Package merge failed"); - RefAssemblyGenerator.GenerateRefAsmsInPackage(Parameters.NugetRoot / "Avalonia." + - Parameters.Version + ".nupkg"); + RefAssemblyGenerator.GenerateRefAsmsInPackage( + Parameters.NugetRoot / $"Avalonia.{Parameters.Version}.nupkg", + Parameters.NugetRoot / $"Avalonia.{Parameters.Version}.snupkg"); }); Target ValidateApiDiff => _ => _ diff --git a/nukebuild/RefAssemblyGenerator.cs b/nukebuild/RefAssemblyGenerator.cs index 54e428c442..e93070e2f0 100644 --- a/nukebuild/RefAssemblyGenerator.cs +++ b/nukebuild/RefAssemblyGenerator.cs @@ -1,8 +1,10 @@ +#nullable enable + +using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; -using ILRepacking; using Mono.Cecil; using Mono.Cecil.Cil; @@ -10,8 +12,8 @@ public class RefAssemblyGenerator { class Resolver : DefaultAssemblyResolver, IAssemblyResolver { - private readonly string _dir; - Dictionary _cache = new(); + readonly string _dir; + readonly Dictionary _cache = new(); public Resolver(string dir) { @@ -31,17 +33,17 @@ public class RefAssemblyGenerator public static void PatchRefAssembly(string file) { - var reader = typeof(RefAssemblyGenerator).Assembly.GetManifestResourceStream("avalonia.snk"); + var reader = typeof(RefAssemblyGenerator).Assembly.GetManifestResourceStream("avalonia.snk")!; var snk = new byte[reader.Length]; - reader.Read(snk, 0, snk.Length); + reader.ReadExactly(snk, 0, snk.Length); var def = AssemblyDefinition.ReadAssembly(file, new ReaderParameters { ReadWrite = true, InMemory = true, ReadSymbols = true, - SymbolReaderProvider = new DefaultSymbolReaderProvider(false), - AssemblyResolver = new Resolver(Path.GetDirectoryName(file)) + SymbolReaderProvider = new DefaultSymbolReaderProvider(throwIfNoSymbol: true), + AssemblyResolver = new Resolver(Path.GetDirectoryName(file)!) }); var obsoleteAttribute = def.MainModule.ImportReference(new TypeReference("System", "ObsoleteAttribute", def.MainModule, @@ -58,7 +60,7 @@ public class RefAssemblyGenerator { StrongNameKeyBlob = snk, WriteSymbols = def.MainModule.HasSymbols, - SymbolWriterProvider = new EmbeddedPortablePdbWriterProvider(), + SymbolWriterProvider = new PortablePdbWriterProvider(), DeterministicMvid = def.MainModule.HasSymbols }); } @@ -146,7 +148,7 @@ public class RefAssemblyGenerator m.Attributes = ((m.Attributes | dflags) ^ dflags) | MethodAttributes.Assembly; } - static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, ICustomAttribute unstableAttribute) + static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, ICustomAttribute? unstableAttribute) { if (def.CustomAttributes.Any(a => a.AttributeType.FullName == "System.ObsoleteAttribute")) return; @@ -172,43 +174,66 @@ public class RefAssemblyGenerator }); } - public static void GenerateRefAsmsInPackage(string packagePath) + public static void GenerateRefAsmsInPackage(string mainPackagePath, string symbolsPackagePath) { - using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), - ZipArchiveMode.Update)) + using var mainArchive = OpenPackage(mainPackagePath); + using var symbolsArchive = OpenPackage(symbolsPackagePath); + + foreach (var entry in mainArchive.Entries + .Where(e => e.FullName.StartsWith("ref/", StringComparison.Ordinal)) + .ToArray()) { - foreach (var entry in archive.Entries.ToList()) - { - if (entry.FullName.StartsWith("ref/")) - entry.Delete(); - } - - foreach (var entry in archive.Entries.ToList()) + entry.Delete(); + } + + foreach (var libEntry in GetLibEntries(mainArchive, ".xml")) + { + var refEntry = mainArchive.CreateEntry("ref/" + libEntry.FullName.Substring(4), CompressionLevel.Optimal); + using var src = libEntry.Open(); + using var dst = refEntry.Open(); + src.CopyTo(dst); + } + + var pdbEntries = GetLibEntries(symbolsArchive, ".pdb").ToDictionary(e => e.FullName); + + var libs = GetLibEntries(mainArchive, ".dll") + .Select(e => (NameParts: e.FullName.Split('/'), Entry: e)) + .Select(e => ( + Tfm: e.NameParts[1], + DllName: e.NameParts[2], + DllEntry: e.Entry, + PdbName: Path.ChangeExtension(e.NameParts[2], ".pdb"), + PdbEntry: pdbEntries.TryGetValue(Path.ChangeExtension(e.Entry.FullName, ".pdb"), out var pdbEntry) ? + pdbEntry : + throw new InvalidOperationException($"Missing symbols for {e.Entry.FullName}"))) + .GroupBy(e => e.Tfm); + + foreach (var tfm in libs) + { + using var _ = Helpers.UseTempDir(out var temp); + + foreach (var lib in tfm) { - if (entry.FullName.StartsWith("lib/") && entry.Name.EndsWith(".xml")) - { - var newEntry = archive.CreateEntry("ref/" + entry.FullName.Substring(4), - CompressionLevel.Optimal); - using (var src = entry.Open()) - using (var dst = newEntry.Open()) - src.CopyTo(dst); - } - } + var extractedDllPath = Path.Combine(temp, lib.DllName); + var extractedPdbPath = Path.Combine(temp, lib.PdbName); + + lib.DllEntry.ExtractToFile(extractedDllPath); + lib.PdbEntry.ExtractToFile(extractedPdbPath); - var libs = archive.Entries.Where(e => e.FullName.StartsWith("lib/") && e.FullName.EndsWith(".dll")) - .Select((e => new { s = e.FullName.Split('/'), e = e })) - .Select(e => new { Tfm = e.s[1], Name = e.s[2], Entry = e.e }) - .GroupBy(x => x.Tfm); - foreach(var tfm in libs) - using (Helpers.UseTempDir(out var temp)) - { - foreach (var l in tfm) - l.Entry.ExtractToFile(Path.Combine(temp, l.Name)); - foreach (var l in tfm) - PatchRefAssembly(Path.Combine(temp, l.Name)); - foreach (var l in tfm) - archive.CreateEntryFromFile(Path.Combine(temp, l.Name), $"ref/{l.Tfm}/{l.Name}"); - } + PatchRefAssembly(extractedDllPath); + + mainArchive.CreateEntryFromFile(extractedDllPath, $"ref/{lib.Tfm}/{lib.DllName}"); + symbolsArchive.CreateEntryFromFile(extractedPdbPath, $"ref/{lib.Tfm}/{lib.PdbName}"); + } } + + static ZipArchive OpenPackage(string packagePath) + => new(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), ZipArchiveMode.Update); + + static ZipArchiveEntry[] GetLibEntries(ZipArchive archive, string extension) + => archive.Entries + .Where(e => e.FullName.StartsWith("lib/", StringComparison.Ordinal) + && e.FullName.EndsWith(extension, StringComparison.Ordinal)) + .ToArray(); } } From adcaf6a317038033391f3d6a22e4b702118eea1b Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Thu, 3 Aug 2023 22:38:52 +0300 Subject: [PATCH 06/12] fix: try 1 --- src/Avalonia.Controls/TopLevel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 04a5a0e6aa..66e402d642 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -591,6 +591,7 @@ namespace Avalonia.Controls Renderer.SceneInvalidated -= SceneInvalidated; // We need to wait for the renderer to complete any in-flight operations Renderer.Dispose(); + StopRendering(); Debug.Assert(PlatformImpl != null); // The PlatformImpl is completely invalid at this point From b0f6d17c558fd7ece0c3d56d090433087de66871 Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Sat, 5 Aug 2023 00:31:33 +0300 Subject: [PATCH 07/12] unit tests added --- .../WindowDataContextTests.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/Avalonia.LeakTests/WindowDataContextTests.cs diff --git a/tests/Avalonia.LeakTests/WindowDataContextTests.cs b/tests/Avalonia.LeakTests/WindowDataContextTests.cs new file mode 100644 index 0000000000..239b090515 --- /dev/null +++ b/tests/Avalonia.LeakTests/WindowDataContextTests.cs @@ -0,0 +1,70 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Reactive; +using Avalonia.Threading; +using Avalonia.UnitTests; +using JetBrains.dotMemoryUnit; +using Xunit; +using Xunit.Abstractions; + +namespace Avalonia.LeakTests; + +internal class ViewModelForDisposingTest +{ + ~ViewModelForDisposingTest() { ; } +} + +[DotMemoryUnit(FailIfRunWithoutSupport = false)] +public class WindowDataContextTests +{ + public WindowDataContextTests(ITestOutputHelper atr) + { + DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); + } + + [Fact] + public void Window_DataContext_Disposed_After_Window_Close_With_Lifetime() + { + static IDisposable Run() + { + var unitTestApp = UnitTestApplication.Start(TestServices.StyledWindow); + var lifetime = new ClassicDesktopStyleApplicationLifetime(); + lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; + var window = new Window { DataContext = new ViewModelForDisposingTest() }; + window.Show(); + window.Close(); + + return Disposable.Create(lifetime, lt => lt.Shutdown()) + .DisposeWith(new CompositeDisposable(lifetime, unitTestApp)); + } + + using var _ = Run(); + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + GC.Collect(); + + dotMemory.Check(m => Assert.Equal(0, + m.GetObjects(o => o.Type.Is()).ObjectsCount)); + } + + [Fact] + public void Window_DataContext_Disposed_After_Window_Close_Without_Lifetime() + { + static void Run() + { + using var _ = UnitTestApplication.Start(TestServices.StyledWindow); + var window = new Window { DataContext = new ViewModelForDisposingTest() }; + window.Show(); + window.Close(); + } + + Run(); + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + GC.Collect(); + + dotMemory.Check(m => Assert.Equal(0, + m.GetObjects(o => o.Type.Is()).ObjectsCount)); + } +} From c418442d1345ee8ea5ed113ae9453bd740088c08 Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Sat, 5 Aug 2023 17:17:57 +0300 Subject: [PATCH 08/12] Revert "unit tests added" This reverts commit b0f6d17c558fd7ece0c3d56d090433087de66871. --- .../WindowDataContextTests.cs | 70 ------------------- 1 file changed, 70 deletions(-) delete mode 100644 tests/Avalonia.LeakTests/WindowDataContextTests.cs diff --git a/tests/Avalonia.LeakTests/WindowDataContextTests.cs b/tests/Avalonia.LeakTests/WindowDataContextTests.cs deleted file mode 100644 index 239b090515..0000000000 --- a/tests/Avalonia.LeakTests/WindowDataContextTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Reactive; -using Avalonia.Threading; -using Avalonia.UnitTests; -using JetBrains.dotMemoryUnit; -using Xunit; -using Xunit.Abstractions; - -namespace Avalonia.LeakTests; - -internal class ViewModelForDisposingTest -{ - ~ViewModelForDisposingTest() { ; } -} - -[DotMemoryUnit(FailIfRunWithoutSupport = false)] -public class WindowDataContextTests -{ - public WindowDataContextTests(ITestOutputHelper atr) - { - DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); - } - - [Fact] - public void Window_DataContext_Disposed_After_Window_Close_With_Lifetime() - { - static IDisposable Run() - { - var unitTestApp = UnitTestApplication.Start(TestServices.StyledWindow); - var lifetime = new ClassicDesktopStyleApplicationLifetime(); - lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; - var window = new Window { DataContext = new ViewModelForDisposingTest() }; - window.Show(); - window.Close(); - - return Disposable.Create(lifetime, lt => lt.Shutdown()) - .DisposeWith(new CompositeDisposable(lifetime, unitTestApp)); - } - - using var _ = Run(); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); - GC.Collect(); - - dotMemory.Check(m => Assert.Equal(0, - m.GetObjects(o => o.Type.Is()).ObjectsCount)); - } - - [Fact] - public void Window_DataContext_Disposed_After_Window_Close_Without_Lifetime() - { - static void Run() - { - using var _ = UnitTestApplication.Start(TestServices.StyledWindow); - var window = new Window { DataContext = new ViewModelForDisposingTest() }; - window.Show(); - window.Close(); - } - - Run(); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); - GC.Collect(); - - dotMemory.Check(m => Assert.Equal(0, - m.GetObjects(o => o.Type.Is()).ObjectsCount)); - } -} From ca2ca4ee9fae2e20155da6bd3148dc3e999fe57c Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Sat, 5 Aug 2023 17:20:34 +0300 Subject: [PATCH 09/12] Revert "fix: try 1" This reverts commit adcaf6a317038033391f3d6a22e4b702118eea1b. --- src/Avalonia.Controls/TopLevel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 66e402d642..04a5a0e6aa 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -591,7 +591,6 @@ namespace Avalonia.Controls Renderer.SceneInvalidated -= SceneInvalidated; // We need to wait for the renderer to complete any in-flight operations Renderer.Dispose(); - StopRendering(); Debug.Assert(PlatformImpl != null); // The PlatformImpl is completely invalid at this point From 5b182890f3c3dece03e81c79ae1bdf889e85961c Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Sat, 5 Aug 2023 17:27:31 +0300 Subject: [PATCH 10/12] Added unit tests for non-disposable DataContext issue (#12123) --- tests/Avalonia.LeakTests/DataContextTests.cs | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/Avalonia.LeakTests/DataContextTests.cs diff --git a/tests/Avalonia.LeakTests/DataContextTests.cs b/tests/Avalonia.LeakTests/DataContextTests.cs new file mode 100644 index 0000000000..d16f8c1f57 --- /dev/null +++ b/tests/Avalonia.LeakTests/DataContextTests.cs @@ -0,0 +1,70 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Reactive; +using Avalonia.Threading; +using Avalonia.UnitTests; +using JetBrains.dotMemoryUnit; +using Xunit; +using Xunit.Abstractions; + +namespace Avalonia.LeakTests; + +internal class ViewModelForDisposingTest +{ + ~ViewModelForDisposingTest() { ; } +} + +[DotMemoryUnit(FailIfRunWithoutSupport = false)] +public class DataContextTests +{ + public DataContextTests(ITestOutputHelper atr) + { + DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); + } + + [Fact] + public void Window_DataContext_Disposed_After_Window_Close_With_Lifetime() + { + static IDisposable Run() + { + var unitTestApp = UnitTestApplication.Start(TestServices.StyledWindow); + var lifetime = new ClassicDesktopStyleApplicationLifetime(); + lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; + var window = new Window { DataContext = new ViewModelForDisposingTest() }; + window.Show(); + window.Close(); + + return Disposable.Create(lifetime, lt => lt.Shutdown()) + .DisposeWith(new CompositeDisposable(lifetime, unitTestApp)); + } + + using var _ = Run(); + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + GC.Collect(); + + dotMemory.Check(m => Assert.Equal(0, + m.GetObjects(o => o.Type.Is()).ObjectsCount)); + } + + [Fact] + public void Window_DataContext_Disposed_After_Window_Close_Without_Lifetime() + { + static void Run() + { + using var _ = UnitTestApplication.Start(TestServices.StyledWindow); + var window = new Window { DataContext = new ViewModelForDisposingTest() }; + window.Show(); + window.Close(); + } + + Run(); + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + GC.Collect(); + + dotMemory.Check(m => Assert.Equal(0, + m.GetObjects(o => o.Type.Is()).ObjectsCount)); + } +} From 628455ec3d3291083d76216de9d9b7934d1fcfcd Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Sat, 5 Aug 2023 17:29:17 +0300 Subject: [PATCH 11/12] To Avalonia.Controls.TopLevel.HandleClosed method body was added Avalonia.Controls.TopLevel.StopRendering method call --- src/Avalonia.Controls/TopLevel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 04a5a0e6aa..66e402d642 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -591,6 +591,7 @@ namespace Avalonia.Controls Renderer.SceneInvalidated -= SceneInvalidated; // We need to wait for the renderer to complete any in-flight operations Renderer.Dispose(); + StopRendering(); Debug.Assert(PlatformImpl != null); // The PlatformImpl is completely invalid at this point From 8328cc79e2bbe0d1b038d689a47df1ddea907665 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 8 Aug 2023 15:25:00 +0600 Subject: [PATCH 12/12] Unwrap win32 data object --- src/Windows/Avalonia.Win32/DataObject.cs | 1 + src/Windows/Avalonia.Win32/OleDropTarget.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs index a215a0a322..df19cdc329 100644 --- a/src/Windows/Avalonia.Win32/DataObject.cs +++ b/src/Windows/Avalonia.Win32/DataObject.cs @@ -103,6 +103,7 @@ namespace Avalonia.Win32 private IDataObject _wrapped; + public IDataObject Wrapped => _wrapped; public DataObject(IDataObject wrapped) { diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index a81652ffc2..94d744301a 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -217,7 +217,7 @@ namespace Avalonia.Win32 if (MicroComRuntime.TryUnwrapManagedObject(pDataObj) is DataObject dataObject) { - return dataObject; + return dataObject.Wrapped; } return new OleDataObject(pDataObj); }