Browse Source

Merge branch 'master' into tcc-fixes

pull/12173/head
Daniel Mayost 3 years ago
committed by GitHub
parent
commit
79e7e26365
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      native/Avalonia.Native/src/OSX/automation.mm
  2. 5
      nukebuild/Build.cs
  3. 109
      nukebuild/RefAssemblyGenerator.cs
  4. 2
      packages/Avalonia/Avalonia.csproj
  5. 5
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  6. 26
      src/Avalonia.Base/Animation/CrossFade.cs
  7. 23
      src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
  8. 7
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  9. 2
      src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs
  10. 2
      src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs
  11. 17
      src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs
  12. 33
      src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs
  13. 25
      src/Avalonia.Controls/Automation/Provider/IRootProvider.cs
  14. 4
      src/Avalonia.Controls/HotkeyManager.cs
  15. 15
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  16. 1
      src/Avalonia.Controls/TopLevel.cs
  17. 12
      src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
  18. 121
      src/Avalonia.Native/AvnAutomationPeer.cs
  19. 7
      src/Avalonia.Native/avn.idl
  20. 1
      src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml
  21. 1
      src/Avalonia.Themes.Simple/Controls/NumericUpDown.xaml
  22. 36
      src/Avalonia.X11/X11Window.cs
  23. 75
      src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
  24. 48
      src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs
  25. 1
      src/Windows/Avalonia.Win32/DataObject.cs
  26. 2
      src/Windows/Avalonia.Win32/OleDropTarget.cs
  27. 122
      tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs
  28. 70
      tests/Avalonia.LeakTests/DataContextTests.cs

11
native/Avalonia.Native/src/OSX/automation.mm

@ -73,6 +73,13 @@ private:
if (peer->IsRootProvider())
{
auto window = peer->RootProvider_GetWindow();
if (window == nullptr)
{
NSLog(@"IRootProvider.PlatformImpl returned null or a non-WindowBaseImpl.");
return nil;
}
auto holder = dynamic_cast<INSWindowHolder*>(window);
auto view = holder->GetNSView();
return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view];
@ -284,8 +291,8 @@ private:
- (id)accessibilityWindow
{
id topLevel = [self accessibilityTopLevelUIElement];
return [topLevel isKindOfClass:[NSWindow class]] ? topLevel : nil;
auto rootPeer = _peer->GetVisualRoot();
return [AvnAccessibilityElement acquire:rootPeer];
}
- (BOOL)isAccessibilityExpanded

5
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 => _ => _

109
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<string, AssemblyDefinition> _cache = new();
readonly string _dir;
readonly Dictionary<string, AssemblyDefinition> _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();
}
}

2
packages/Avalonia/Avalonia.csproj

@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.BuildServices" Version="0.0.28" />
<PackageReference Include="Avalonia.BuildServices" Version="0.0.29" />
<ProjectReference Include="../../src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj" />
<ProjectReference Include="../../src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj">
<PrivateAssets>all</PrivateAssets>

5
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);

26
src/Avalonia.Base/Animation/CrossFade.cs

@ -35,6 +35,18 @@ namespace Avalonia.Animation
{
Children =
{
new KeyFrame()
{
Setters =
{
new Setter
{
Property = Visual.OpacityProperty,
Value = 1d
}
},
Cue = new Cue(0d)
},
new KeyFrame()
{
Setters =
@ -54,6 +66,18 @@ namespace Avalonia.Animation
{
Children =
{
new KeyFrame()
{
Setters =
{
new Setter
{
Property = Visual.OpacityProperty,
Value = 0d
}
},
Cue = new Cue(0d)
},
new KeyFrame()
{
Setters =
@ -117,11 +141,13 @@ namespace Avalonia.Animation
if (from != null)
{
from.Opacity = 0f;
tasks.Add(_fadeOutAnimation.RunAsync(from, null, cancellationToken));
}
if (to != null)
{
to.Opacity = 1f;
to.IsVisible = true;
tasks.Add(_fadeInAnimation.RunAsync(to, null, cancellationToken));
}

23
src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Avalonia.Automation.Provider;
namespace Avalonia.Automation.Peers
{
@ -115,9 +116,14 @@ namespace Avalonia.Automation.Peers
/// <summary>
/// Gets the <see cref="AutomationPeer"/> that is the parent of this <see cref="AutomationPeer"/>.
/// </summary>
/// <returns></returns>
public AutomationPeer? GetParent() => GetParentCore();
/// <summary>
/// Gets the <see cref="AutomationPeer"/> that is the root of this <see cref="AutomationPeer"/>'s
/// visual tree.
/// </summary>
public AutomationPeer? GetVisualRoot() => GetVisualRootCore();
/// <summary>
/// Gets a value that indicates whether the element that is associated with this automation
/// peer currently has keyboard focus.
@ -247,6 +253,21 @@ namespace Avalonia.Automation.Peers
return GetAutomationControlTypeCore();
}
protected virtual AutomationPeer? GetVisualRootCore()
{
var peer = this;
var parent = peer.GetParent();
while (peer.GetProvider<IRootProvider>() is null && parent is not null)
{
peer = parent;
parent = peer.GetParent();
}
return peer;
}
protected virtual bool IsContentElementOverrideCore()
{
return IsControlElement() && IsContentElementCore();

7
src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs

@ -120,6 +120,13 @@ namespace Avalonia.Automation.Peers
return _parent;
}
protected override AutomationPeer? GetVisualRootCore()
{
if (Owner.GetVisualRoot() is Control c)
return CreatePeerForElement(c);
return null;
}
/// <summary>
/// Invalidates the peer's children and causes a re-read from <see cref="GetChildrenCore"/>.
/// </summary>

2
src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs

@ -28,7 +28,7 @@ namespace Avalonia.Automation.Peers
if (!_searchedForScrollable)
{
if (Owner.GetValue(ListBox.ScrollProperty) is Control scrollable)
_scroller = GetOrCreate(scrollable) as IScrollProvider;
_scroller = GetOrCreate(scrollable).GetProvider<IScrollProvider>();
_searchedForScrollable = true;
}

2
src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs

@ -22,7 +22,7 @@ namespace Avalonia.Automation.Peers
if (Owner.Parent is Control parent)
{
var parentPeer = GetOrCreate(parent);
return parentPeer as ISelectionProvider;
return parentPeer.GetProvider<ISelectionProvider>();
}
return null;

17
src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Globalization;
using Avalonia.Automation.Provider;
using Avalonia.Controls;
using Avalonia.Input;
@ -32,7 +33,21 @@ namespace Avalonia.Automation.Peers
public AutomationPeer? GetPeerFromPoint(Point p)
{
var hit = Owner.GetVisualAt(p)?.FindAncestorOfType<Control>(includeSelf: true);
return hit is object ? GetOrCreate(hit) : null;
if (hit is null)
return null;
var peer = GetOrCreate(hit);
while (peer != this && peer.GetProvider<IEmbeddedRootProvider>() is { } embedded)
{
var embeddedHit = embedded.GetPeerFromPoint(p);
if (embeddedHit is null)
break;
peer = embeddedHit;
}
return peer;
}
protected void StartTrackingFocus()

33
src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs

@ -0,0 +1,33 @@
using System;
using Avalonia.Automation.Peers;
namespace Avalonia.Automation.Provider
{
/// <summary>
/// Exposure methods and properties to support UI Automation client access to the root of an
/// automation tree hosted by another UI framework.
/// </summary>
/// <remarks>
/// This interface is implemented by the <see cref="AutomationPeer"/> class, and can be used
/// to embed an automation tree from a 3rd party UI framework that wishes to use Avalonia's
/// automation support.
/// </remarks>
public interface IEmbeddedRootProvider
{
/// <summary>
/// Gets the currently focused element.
/// </summary>
AutomationPeer? GetFocus();
/// <summary>
/// Gets the element at the specified point, expressed in top-level coordinates.
/// </summary>
/// <param name="p">The point.</param>
AutomationPeer? GetPeerFromPoint(Point p);
/// <summary>
/// Raised by the automation peer when the focus changes.
/// </summary>
event EventHandler? FocusChanged;
}
}

25
src/Avalonia.Controls/Automation/Provider/IRootProvider.cs

@ -4,11 +4,36 @@ using Avalonia.Platform;
namespace Avalonia.Automation.Provider
{
/// <summary>
/// Exposes methods and properties to support UI Automation client access to the root of an
/// automation tree.
/// </summary>
/// <remarks>
/// This interface is implemented by the <see cref="AutomationPeer"/> class, and should only
/// be implemented on true root elements, such as Windows. To embed an automation tree, use
/// <see cref="IEmbeddedRootProvider"/> instead.
/// </remarks>
public interface IRootProvider
{
/// <summary>
/// Gets the platform implementation of the TopLevel for the element.
/// </summary>
ITopLevelImpl? PlatformImpl { get; }
/// <summary>
/// Gets the currently focused element.
/// </summary>
AutomationPeer? GetFocus();
/// <summary>
/// Gets the element at the specified point, expressed in top-level coordinates.
/// </summary>
/// <param name="p">The point.</param>
AutomationPeer? GetPeerFromPoint(Point p);
/// <summary>
/// Raised by the automation peer when the focus changes.
/// </summary>
event EventHandler? FocusChanged;
}
}

4
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;
}

15
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -126,6 +126,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
ContentControl.VerticalContentAlignmentProperty.AddOwner<NumericUpDown>();
/// <summary>
/// Defines the <see cref="TextAlignment"/> property
/// </summary>
public static readonly StyledProperty<Media.TextAlignment> TextAlignmentProperty =
TextBox.TextAlignmentProperty.AddOwner<NumericUpDown>();
private IDisposable? _textBoxTextChangedSubscription;
private bool _internalValueSet;
@ -299,6 +305,15 @@ namespace Avalonia.Controls
set => SetValue(VerticalContentAlignmentProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="Media.TextAlignment"/> of the <see cref="NumericUpDown"/>
/// </summary>
public Media.TextAlignment TextAlignment
{
get => GetValue(TextAlignmentProperty);
set => SetValue(TextAlignmentProperty, value);
}
/// <summary>
/// Initializes new instance of <see cref="NumericUpDown"/> class.
/// </summary>

1
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

12
src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs

@ -62,9 +62,15 @@ namespace Avalonia.FreeDesktop.DBusIme
foreach (var name in _knownNames)
{
var dbus = new OrgFreedesktopDBus(Connection, "org.freedesktop.DBus", "/org/freedesktop/DBus");
_disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange));
var nameOwner = await dbus.GetNameOwnerAsync(name);
OnNameChange(null, (name, null, nameOwner));
try
{
_disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange));
var nameOwner = await dbus.GetNameOwnerAsync(name);
OnNameChange(null, (name, null, nameOwner));
}
catch (DBusException)
{
}
}
}

121
src/Avalonia.Native/AvnAutomationPeer.cs

@ -22,8 +22,8 @@ namespace Avalonia.Native
{
_inner = inner;
_inner.ChildrenChanged += (_, _) => Node?.ChildrenChanged();
if (inner is WindowBaseAutomationPeer window)
window.FocusChanged += (_, _) => Node?.FocusChanged();
if (inner is IRootProvider root)
root.FocusChanged += (_, _) => Node?.FocusChanged();
}
~AvnAutomationPeer() => Node?.Dispose();
@ -39,6 +39,7 @@ namespace Avalonia.Native
public IAvnAutomationPeer? LabeledBy => Wrap(_inner.GetLabeledBy());
public IAvnString Name => _inner.GetName().ToAvnString();
public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent());
public IAvnAutomationPeer? VisualRoot => Wrap(_inner.GetVisualRoot());
public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool();
public int IsContentElement() => _inner.IsContentElement().AsComBool();
@ -48,6 +49,13 @@ namespace Avalonia.Native
public void SetFocus() => _inner.SetFocus();
public int ShowContextMenu() => _inner.ShowContextMenu().AsComBool();
public void SetNode(IAvnAutomationNode node)
{
if (Node is not null)
throw new InvalidOperationException("The AvnAutomationPeer already has a node.");
Node = node;
}
public IAvnAutomationPeer? RootPeer
{
get
@ -55,7 +63,7 @@ namespace Avalonia.Native
var peer = _inner;
var parent = peer.GetParent();
while (peer is not IRootProvider && parent is not null)
while (peer.GetProvider<IRootProvider>() is null && parent is not null)
{
peer = parent;
parent = peer.GetParent();
@ -65,26 +73,23 @@ namespace Avalonia.Native
}
}
public void SetNode(IAvnAutomationNode node)
{
if (Node is not null)
throw new InvalidOperationException("The AvnAutomationPeer already has a node.");
Node = node;
}
public int IsRootProvider() => (_inner is IRootProvider).AsComBool();
private IEmbeddedRootProvider EmbeddedRootProvider => GetProvider<IEmbeddedRootProvider>();
private IExpandCollapseProvider ExpandCollapseProvider => GetProvider<IExpandCollapseProvider>();
private IInvokeProvider InvokeProvider => GetProvider<IInvokeProvider>();
private IRangeValueProvider RangeValueProvider => GetProvider<IRangeValueProvider>();
private IRootProvider RootProvider => GetProvider<IRootProvider>();
private ISelectionItemProvider SelectionItemProvider => GetProvider<ISelectionItemProvider>();
private IToggleProvider ToggleProvider => GetProvider<IToggleProvider>();
private IValueProvider ValueProvider => GetProvider<IValueProvider>();
public IAvnWindowBase RootProvider_GetWindow()
{
var window = (WindowBase)((ControlAutomationPeer)_inner).Owner;
return ((WindowBaseImpl)window.PlatformImpl!).Native;
}
public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus());
public int IsRootProvider() => IsProvider<IRootProvider>();
public IAvnWindowBase? RootProvider_GetWindow() => (RootProvider.PlatformImpl as WindowBaseImpl)?.Native;
public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(RootProvider.GetFocus());
public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point)
{
var result = ((IRootProvider)_inner).GetPeerFromPoint(point.ToAvaloniaPoint());
var result = RootProvider.GetPeerFromPoint(point.ToAvaloniaPoint());
if (result is null)
return null;
@ -103,46 +108,80 @@ namespace Avalonia.Native
return Wrap(result);
}
public int IsExpandCollapseProvider() => (_inner is IExpandCollapseProvider).AsComBool();
public int ExpandCollapseProvider_GetIsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch
public int IsEmbeddedRootProvider() => IsProvider<IEmbeddedRootProvider>();
public IAvnAutomationPeer? EmbeddedRootProvider_GetFocus() => Wrap(EmbeddedRootProvider.GetFocus());
public IAvnAutomationPeer? EmbeddedRootProvider_GetPeerFromPoint(AvnPoint point)
{
var result = EmbeddedRootProvider.GetPeerFromPoint(point.ToAvaloniaPoint());
if (result is null)
return null;
// The OSX accessibility APIs expect non-ignored elements when hit-testing.
while (!result.IsControlElement())
{
var parent = result.GetParent();
if (parent is not null)
result = parent;
else
break;
}
return Wrap(result);
}
public int IsExpandCollapseProvider() => IsProvider<IExpandCollapseProvider>();
public int ExpandCollapseProvider_GetIsExpanded() => ExpandCollapseProvider.ExpandCollapseState switch
{
ExpandCollapseState.Expanded => 1,
ExpandCollapseState.PartiallyExpanded => 1,
_ => 0,
};
public int ExpandCollapseProvider_GetShowsMenu() => ((IExpandCollapseProvider)_inner).ShowsMenu.AsComBool();
public void ExpandCollapseProvider_Expand() => ((IExpandCollapseProvider)_inner).Expand();
public void ExpandCollapseProvider_Collapse() => ((IExpandCollapseProvider)_inner).Collapse();
public int ExpandCollapseProvider_GetShowsMenu() => ExpandCollapseProvider.ShowsMenu.AsComBool();
public void ExpandCollapseProvider_Expand() => ExpandCollapseProvider.Expand();
public void ExpandCollapseProvider_Collapse() => ExpandCollapseProvider.Collapse();
public int IsInvokeProvider() => (_inner is IInvokeProvider).AsComBool();
public void InvokeProvider_Invoke() => ((IInvokeProvider)_inner).Invoke();
public int IsInvokeProvider() => IsProvider<IInvokeProvider>();
public void InvokeProvider_Invoke() => InvokeProvider.Invoke();
public int IsRangeValueProvider() => (_inner is IRangeValueProvider).AsComBool();
public double RangeValueProvider_GetValue() => ((IRangeValueProvider)_inner).Value;
public double RangeValueProvider_GetMinimum() => ((IRangeValueProvider)_inner).Minimum;
public double RangeValueProvider_GetMaximum() => ((IRangeValueProvider)_inner).Maximum;
public double RangeValueProvider_GetSmallChange() => ((IRangeValueProvider)_inner).SmallChange;
public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange;
public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value);
public int IsRangeValueProvider() => IsProvider<IRangeValueProvider>();
public double RangeValueProvider_GetValue() => RangeValueProvider.Value;
public double RangeValueProvider_GetMinimum() => RangeValueProvider.Minimum;
public double RangeValueProvider_GetMaximum() => RangeValueProvider.Maximum;
public double RangeValueProvider_GetSmallChange() => RangeValueProvider.SmallChange;
public double RangeValueProvider_GetLargeChange() => RangeValueProvider.LargeChange;
public void RangeValueProvider_SetValue(double value) => RangeValueProvider.SetValue(value);
public int IsSelectionItemProvider() => (_inner is ISelectionItemProvider).AsComBool();
public int SelectionItemProvider_IsSelected() => ((ISelectionItemProvider)_inner).IsSelected.AsComBool();
public int IsSelectionItemProvider() => IsProvider<ISelectionItemProvider>();
public int SelectionItemProvider_IsSelected() => SelectionItemProvider.IsSelected.AsComBool();
public int IsToggleProvider() => (_inner is IToggleProvider).AsComBool();
public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState;
public void ToggleProvider_Toggle() => ((IToggleProvider)_inner).Toggle();
public int IsToggleProvider() => IsProvider<IToggleProvider>();
public int ToggleProvider_GetToggleState() => (int)ToggleProvider.ToggleState;
public void ToggleProvider_Toggle() => ToggleProvider.Toggle();
public int IsValueProvider() => (_inner is IValueProvider).AsComBool();
public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString();
public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value);
public int IsValueProvider() => IsProvider<IValueProvider>();
public IAvnString ValueProvider_GetValue() => ValueProvider.Value.ToAvnString();
public void ValueProvider_SetValue(string value) => ValueProvider.SetValue(value);
[return: NotNullIfNotNull("peer")]
public static AvnAutomationPeer? Wrap(AutomationPeer? peer)
{
return peer is null ? null : s_wrappers.GetValue(peer, x => new(peer));
}
private T GetProvider<T>()
{
return _inner.GetProvider<T>() ?? throw new InvalidOperationException(
$"The peer {_inner} does not implement {typeof(T)}.");
}
private int IsProvider<T>() => (_inner.GetProvider<T>() is not null).AsComBool();
}
internal class AvnAutomationPeerArray : NativeCallbackBase, IAvnAutomationPeerArray

7
src/Avalonia.Native/avn.idl

@ -921,6 +921,7 @@ interface IAvnAutomationPeer : IUnknown
IAvnAutomationPeer* GetLabeledBy();
IAvnString* GetName();
IAvnAutomationPeer* GetParent();
IAvnAutomationPeer* GetVisualRoot();
bool HasKeyboardFocus();
bool IsContentElement();
bool IsControlElement();
@ -935,7 +936,11 @@ interface IAvnAutomationPeer : IUnknown
IAvnWindowBase* RootProvider_GetWindow();
IAvnAutomationPeer* RootProvider_GetFocus();
IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point);
bool IsEmbeddedRootProvider();
IAvnAutomationPeer* EmbeddedRootProvider_GetFocus();
IAvnAutomationPeer* EmbeddedRootProvider_GetPeerFromPoint(AvnPoint point);
bool IsExpandCollapseProvider();
bool ExpandCollapseProvider_GetIsExpanded();
bool ExpandCollapseProvider_GetShowsMenu();

1
src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml

@ -57,6 +57,7 @@
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
Text="{TemplateBinding Text}"
TextAlignment="{TemplateBinding TextAlignment}"
AcceptsReturn="False"
TextWrapping="NoWrap" />
</ButtonSpinner>

1
src/Avalonia.Themes.Simple/Controls/NumericUpDown.xaml

@ -31,6 +31,7 @@
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
IsReadOnly="{TemplateBinding IsReadOnly}"
Text="{TemplateBinding Text}"
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="NoWrap"
Watermark="{TemplateBinding Watermark}" />
</ButtonSpinner>

36
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<object>
{
@ -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);

75
src/Windows/Avalonia.Win32/Automation/AutomationNode.cs

@ -54,25 +54,11 @@ namespace Avalonia.Win32.Automation
_runtimeId = new int[] { 3, GetHashCode() };
Peer = peer;
s_nodes.Add(peer, this);
peer.ChildrenChanged += Peer_ChildrenChanged;
peer.PropertyChanged += Peer_PropertyChanged;
}
private void Peer_ChildrenChanged(object? sender, EventArgs e)
{
ChildrenChanged();
}
peer.ChildrenChanged += OnPeerChildrenChanged;
peer.PropertyChanged += OnPeerPropertyChanged;
private void Peer_PropertyChanged(object? sender, AutomationPropertyChangedEventArgs e)
{
if (s_propertyMap.TryGetValue(e.Property, out var id))
{
UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(
this,
(int)id,
e.OldValue as IConvertible,
e.NewValue as IConvertible);
}
if (Peer.GetProvider<AAP.IEmbeddedRootProvider>() is { } embeddedRoot)
embeddedRoot.FocusChanged += OnEmbeddedRootFocusChanged;
}
public AutomationPeer Peer { get; protected set; }
@ -89,21 +75,12 @@ namespace Avalonia.Win32.Automation
public virtual IRawElementProviderFragmentRoot? FragmentRoot
{
get => InvokeSync(() => GetRoot()) as IRawElementProviderFragmentRoot;
get => InvokeSync(() => GetRoot());
}
public virtual IRawElementProviderSimple? HostRawElementProvider => null;
public ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider;
public void ChildrenChanged()
{
UiaCoreProviderApi.UiaRaiseStructureChangedEvent(
this,
StructureChangeType.ChildrenInvalidated,
null,
0);
}
[return: MarshalAs(UnmanagedType.IUnknown)]
public virtual object? GetPatternProvider(int patternId)
{
@ -250,21 +227,49 @@ namespace Avalonia.Win32.Automation
throw new NotSupportedException();
}
protected void RaiseChildrenChanged()
{
UiaCoreProviderApi.UiaRaiseStructureChangedEvent(
this,
StructureChangeType.ChildrenInvalidated,
null,
0);
}
private AutomationNode? GetRoot()
protected void RaiseFocusChanged(AutomationNode? focused)
{
UiaCoreProviderApi.UiaRaiseAutomationEvent(
focused,
(int)UiaEventId.AutomationFocusChanged);
}
private RootAutomationNode? GetRoot()
{
Dispatcher.UIThread.VerifyAccess();
return GetOrCreate(Peer.GetVisualRoot()) as RootAutomationNode;
}
var peer = Peer;
var parent = peer.GetParent();
private void OnPeerChildrenChanged(object? sender, EventArgs e)
{
RaiseChildrenChanged();
}
while (peer.GetProvider<AAP.IRootProvider>() is null && parent is object)
private void OnPeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e)
{
if (s_propertyMap.TryGetValue(e.Property, out var id))
{
peer = parent;
parent = peer.GetParent();
UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(
this,
(int)id,
e.OldValue as IConvertible,
e.NewValue as IConvertible);
}
}
return peer is object ? GetOrCreate(peer) : null;
private void OnEmbeddedRootFocusChanged(object? sender, EventArgs e)
{
if (Peer.GetProvider<AAP.IEmbeddedRootProvider>() is { } embeddedRoot)
RaiseFocusChanged(GetOrCreate(embeddedRoot.GetFocus()));
}
private static AutomationNode Create(AutomationPeer peer)

48
src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs

@ -9,18 +9,14 @@ using Avalonia.Win32.Interop.Automation;
namespace Avalonia.Win32.Automation
{
[RequiresUnreferencedCode("Requires .NET COM interop")]
internal class RootAutomationNode : AutomationNode,
IRawElementProviderFragmentRoot,
IRawElementProviderAdviseEvents
internal class RootAutomationNode : AutomationNode, IRawElementProviderFragmentRoot
{
private int _raiseFocusChanged;
public RootAutomationNode(AutomationPeer peer)
: base(peer)
{
Peer = base.Peer.GetProvider<IRootProvider>() ?? throw new AvaloniaInternalException(
"Attempt to create RootAutomationNode from peer which does not implement IRootProvider.");
Peer.FocusChanged += FocusChanged;
Peer.FocusChanged += OnRootFocusChanged;
}
public override IRawElementProviderFragmentRoot? FragmentRoot => this;
@ -44,41 +40,6 @@ namespace Avalonia.Win32.Automation
return GetOrCreate(focus);
}
void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties)
{
switch ((UiaEventId)eventId)
{
case UiaEventId.AutomationFocusChanged:
++_raiseFocusChanged;
break;
}
}
void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties)
{
switch ((UiaEventId)eventId)
{
case UiaEventId.AutomationFocusChanged:
--_raiseFocusChanged;
break;
}
}
protected void RaiseFocusChanged(AutomationNode? focused)
{
if (_raiseFocusChanged > 0)
{
UiaCoreProviderApi.UiaRaiseAutomationEvent(
focused,
(int)UiaEventId.AutomationFocusChanged);
}
}
public void FocusChanged(object? sender, EventArgs e)
{
RaiseFocusChanged(GetOrCreate(Peer.GetFocus()));
}
public Rect ToScreen(Rect rect)
{
if (WindowImpl is null)
@ -101,5 +62,10 @@ namespace Avalonia.Win32.Automation
return result;
}
}
private void OnRootFocusChanged(object? sender, EventArgs e)
{
RaiseFocusChanged(GetOrCreate(Peer.GetFocus()));
}
}
}

1
src/Windows/Avalonia.Win32/DataObject.cs

@ -103,6 +103,7 @@ namespace Avalonia.Win32
private IDataObject _wrapped;
public IDataObject Wrapped => _wrapped;
public DataObject(IDataObject wrapped)
{

2
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);
}

122
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<KeyGesture> HotKeyProperty =
HotKeyManager.HotKeyProperty.AddOwner<HotKeyedTextBox>();
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<IWindowingPlatform>();
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);
}
}
}

70
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<ViewModelForDisposingTest>()).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<ViewModelForDisposingTest>()).ObjectsCount));
}
}
Loading…
Cancel
Save