Browse Source

Merge branch 'master' into fixes/DevTools/WithoutApplicationLifetime

pull/10510/head
Max Katz 3 years ago
committed by GitHub
parent
commit
b0e3d0f116
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      src/Avalonia.Base/Input/FocusManager.cs
  2. 4
      src/Avalonia.Base/Input/InputElement.cs
  3. 2
      src/Avalonia.Base/Input/TextInput/InputMethodManager.cs
  4. 3
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  5. 11
      src/Avalonia.Controls/ComboBox.cs
  6. 10
      src/Avalonia.Controls/Flyouts/MenuFlyout.cs
  7. 101
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  8. 58
      src/Avalonia.Controls/ItemsControl.cs
  9. 11
      src/Avalonia.Controls/ListBox.cs
  10. 18
      src/Avalonia.Controls/MenuBase.cs
  11. 18
      src/Avalonia.Controls/MenuItem.cs
  12. 8
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  13. 11
      src/Avalonia.Controls/Primitives/TabStrip.cs
  14. 11
      src/Avalonia.Controls/TabControl.cs
  15. 11
      src/Avalonia.Controls/TreeView.cs
  16. 8
      src/Avalonia.Controls/TreeViewItem.cs
  17. 97
      src/Avalonia.Controls/VirtualizingCarouselPanel.cs
  18. 112
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  19. 21
      src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
  20. 9
      src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs
  21. 6
      src/Browser/Avalonia.Browser/Interop/StorageHelper.cs
  22. 16
      src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
  23. 8
      src/Browser/Avalonia.Browser/WindowingPlatform.cs
  24. 3
      src/Browser/Avalonia.Browser/webapp/build.js
  25. 9
      src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
  26. 7
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts
  27. 23
      src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts
  28. 78
      src/Browser/Avalonia.Browser/webapp/modules/sw.ts
  29. 50
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs
  30. 10
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs
  31. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  32. 240
      tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
  33. 6
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  34. 1
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
  35. 16
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  36. 1
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  37. 5
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  38. 51
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
  39. 4
      tests/Avalonia.LeakTests/ControlTests.cs
  40. 179
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs
  41. 17
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs

7
src/Avalonia.Base/Input/FocusManager.cs

@ -122,6 +122,11 @@ namespace Avalonia.Input
{
scope = scope ?? throw new ArgumentNullException(nameof(scope));
if (element is not null && !CanFocus(element))
{
return;
}
if (_focusScopes.TryGetValue(scope, out var existingElement))
{
if (element != existingElement)
@ -242,6 +247,6 @@ namespace Avalonia.Input
}
}
private static bool IsVisible(IInputElement e) => (e as Visual)?.IsVisible ?? true;
private static bool IsVisible(IInputElement e) => (e as Visual)?.IsEffectivelyVisible ?? true;
}
}

4
src/Avalonia.Base/Input/InputElement.cs

@ -647,6 +647,10 @@ namespace Avalonia.Input
{
PseudoClasses.Set(":focus-within", change.GetNewValue<bool>());
}
else if (change.Property == IsVisibleProperty && !change.GetNewValue<bool>() && IsFocused)
{
FocusManager.Instance?.Focus(null);
}
}
/// <summary>

2
src/Avalonia.Base/Input/TextInput/InputMethodManager.cs

@ -48,9 +48,9 @@ namespace Avalonia.Input.TextInput
}
_transformTracker.SetVisual(_client?.TextViewVisual);
UpdateCursorRect();
_im?.SetClient(_client);
UpdateCursorRect();
}
else
{

3
src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs

@ -50,7 +50,8 @@ internal static class StorageProviderHelpers
}
}
public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter)
[return: NotNullIfNotNull(nameof(path))]
public static string? NameWithExtension(string? path, string? defaultExtension, FilePickerFileType? filter)
{
var name = Path.GetFileName(path);
if (name != null && !Path.HasExtension(name))

11
src/Avalonia.Controls/ComboBox.cs

@ -170,8 +170,15 @@ namespace Avalonia.Controls
UpdateFlowDirection();
}
protected internal override Control CreateContainerForItemOverride() => new ComboBoxItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ComboBoxItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new ComboBoxItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<ComboBoxItem>(item, out recycleKey);
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)

10
src/Avalonia.Controls/Flyouts/MenuFlyout.cs

@ -24,9 +24,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="ItemTemplate"/> property
/// </summary>
public static readonly DirectProperty<MenuFlyout, IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.RegisterDirect<MenuFlyout, IDataTemplate?>(nameof(ItemTemplate),
x => x.ItemTemplate, (x, v) => x.ItemTemplate = v);
public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.Register<MenuFlyout, IDataTemplate?>(nameof(ItemTemplate));
/// <summary>
/// Defines the <see cref="ItemContainerTheme"/> property.
@ -59,8 +58,8 @@ namespace Avalonia.Controls
/// </summary>
public IDataTemplate? ItemTemplate
{
get => _itemTemplate;
set => SetAndRaise(ItemTemplateProperty, ref _itemTemplate, value);
get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
/// <summary>
@ -82,7 +81,6 @@ namespace Avalonia.Controls
}
private Classes? _classes;
private IDataTemplate? _itemTemplate;
protected override Control CreatePresenter()
{

101
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@ -10,34 +10,44 @@ namespace Avalonia.Controls.Generators
/// When creating a container for an item from a <see cref="VirtualizingPanel"/>, the following
/// process should be followed:
///
/// - <see cref="IsItemItsOwnContainer(Control)"/> should first be called if the item is
/// derived from the <see cref="Control"/> class. If this method returns true then the
/// item itself should be used as the container.
/// - If <see cref="IsItemItsOwnContainer(Control)"/> returns false then
/// <see cref="CreateContainer"/> should be called to create a new container.
/// - <see cref="NeedsContainer(object, int, out object?)"/> should first be called to
/// determine whether the item needs a container. This method will return true if the item
/// should be wrapped in a container control, or false if the item itself can be used as a
/// container.
/// - If <see cref="NeedsContainer(object, int, out object?)"/> returns true then the
/// <see cref="CreateContainer"/> method should be called to create a new container, passing
/// the recycle key returned from <see cref="NeedsContainer(object, int, out object?)"/>.
/// - If the panel supports recycling and the recycle key is non-null then the recycle key
/// should be recorded for the container (e.g. in an attached property or the realized
/// container list).
/// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
/// container.
/// - The container should then be added to the panel using
/// <see cref="VirtualizingPanel.AddInternalChild(Control)"/>
/// - Finally, <see cref="ItemContainerPrepared(Control, object?, int)"/> should be called.
///
/// NOTE: If <see cref="IsItemItsOwnContainer(Control)"/> in the first step above returns true
/// then the above steps should be carried out a single time; the first time the item is
/// displayed. Otherwise the steps should be carried out each time a new container is realized
/// for an item.
/// NOTE: If <see cref="NeedsContainer(object, int, out object?)"/> in the first step above
/// returns false then the above steps should be carried out a single time: the first time the
/// item is displayed. Otherwise the steps should be carried out each time a new container is
/// realized for an item.
///
/// When unrealizing a container, the following process should be followed:
///
/// - If <see cref="IsItemItsOwnContainer(Control)"/> for the item returned true then the item
/// cannot be unrealized or recycled.
/// - If <see cref="NeedsContainer(object, int, out object?)"/> for the item returned false
/// then the item cannot be unrealized or recycled.
/// - Otherwise, <see cref="ClearItemContainer(Control)"/> should be called for the container
/// - If recycling is supported then the container should be added to a recycle pool.
/// - It is assumed that recyclable containers will not be removed from the panel but instead
/// hidden from view using e.g. `container.IsVisible = false`.
/// - If recycling is supported by the panel and the container then the container should be
/// added to a recycle pool keyed on the recycle key returned from
/// <see cref="NeedsContainer(object, int, out object?)"/>. It is assumed that recycled
/// containers will not be removed from the panel but instead hidden from view using
/// e.g. `container.IsVisible = false`.
/// - If recycling is not supported then the container should be removed from the panel.
///
/// When recycling an unrealized container, the following process should be followed:
///
/// - An element should be taken from the recycle pool.
/// - <see cref="NeedsContainer(object, int, out object?)"/> should be called to determine
/// whether the item needs a container, and if so, the recycle key.
/// - A container should be taken from the recycle pool keyed on the returned recycle key.
/// - The container should be made visible.
/// - <see cref="PrepareItemContainer(Control, object?, int)"/> method should be called for the
/// container.
@ -55,28 +65,43 @@ namespace Avalonia.Controls.Generators
internal ItemContainerGenerator(ItemsControl owner) => _owner = owner;
/// <summary>
/// Creates a new container control.
/// Determines whether the specified item needs to be wrapped in a container control.
/// </summary>
/// <returns>The newly created container control.</returns>
/// <remarks>
/// Before calling this method, <see cref="IsItemItsOwnContainer(Control)"/> should be
/// called to determine whether the item itself should be used as a container. After
/// calling this method, <see cref="PrepareItemContainer(Control, object, int)"/> should
/// be called to prepare the container to display the specified item.
/// </remarks>
public Control CreateContainer() => _owner.CreateContainerForItemOverride();
/// <param name="item">The item to display.</param>
/// <param name="index">The index of the item.</param>
/// <param name="recycleKey">
/// When the method returns, contains a key that can be used to locate a previously
/// recycled container of the correct type, or null if the item cannot be recycled.
/// </param>
/// <returns>
/// true if the item needs a container; otherwise false if the item can itself be used
/// as a container.
/// </returns>
public bool NeedsContainer(object? item, int index, out object? recycleKey) =>
_owner.NeedsContainerOverride(item, index, out recycleKey);
/// <summary>
/// Determines whether the specified item is (or is eligible to be) its own container.
/// Creates a new container control.
/// </summary>
/// <param name="container">The item.</param>
/// <returns>true if the item is its own container, otherwise false.</returns>
/// <param name="item">The item to display.</param>
/// <param name="index">The index of the item.</param>
/// <param name="recycleKey">
/// The recycle key returned from <see cref="NeedsContainer(object, int, out object?)"/>
/// </param>
/// <returns>The newly created container control.</returns>
/// <remarks>
/// Whereas in WPF/UWP, non-control items can be their own container, in Avalonia only
/// control items may be; the caller is responsible for checking if each item is a control
/// and calling this method before creating a new container.
/// Before calling this method, <see cref="NeedsContainer(object, int, out object?)"/>
/// should be called to determine whether the item itself should be used as a container.
/// After calling this method, <see cref="PrepareItemContainer(Control, object, int)"/>
/// must be called to prepare the container to display the specified item.
///
/// If the panel supports recycling then the returned recycle key should be stored alongside
/// the container and when container becomes eligible for recycling the container should
/// be placed in a recycle pool using this key. If the returned recycle key is null then
/// the container cannot be recycled.
/// </remarks>
public bool IsItemItsOwnContainer(Control container) => _owner.IsItemItsOwnContainerOverride(container);
public Control CreateContainer(object? item, int index, object? recycleKey)
=> _owner.CreateContainerForItemOverride(item, index, recycleKey);
/// <summary>
/// Prepares the specified element as the container for the corresponding item.
@ -85,10 +110,10 @@ namespace Avalonia.Controls.Generators
/// <param name="item">The item to display.</param>
/// <param name="index">The index of the item to display.</param>
/// <remarks>
/// If <see cref="IsItemItsOwnContainer(Control)"/> is true for an item, then this method
/// must only be called a single time, otherwise this method must be called after the
/// container is created, and each subsequent time the container is recycled to display a
/// new item.
/// If <see cref="NeedsContainer(object, int, out object?)"/> is false for an
/// item, then this method must only be called a single time; otherwise this method must
/// be called after the container is created, and each subsequent time the container is
/// recycled to display a new item.
/// </remarks>
public void PrepareItemContainer(Control container, object? item, int index) =>
_owner.PrepareItemContainer(container, item, index);
@ -104,8 +129,8 @@ namespace Avalonia.Controls.Generators
/// This method must be called when a container has been fully prepared and added
/// to the logical and visual trees, but may be called before a layout pass has completed.
/// It must be called regardless of the result of
/// <see cref="IsItemItsOwnContainer(Control)"/> but if that method returned true then
/// must be called only a single time.
/// <see cref="NeedsContainer(object, int, out object?)"/> but if that method returned
/// false then must be called only a single time.
/// </remarks>
public void ItemContainerPrepared(Control container, object? item, int index) =>
_owner.ItemContainerPrepared(container, item, index);
@ -128,7 +153,7 @@ namespace Avalonia.Controls.Generators
/// This method must be called when a container is unrealized. The container must have
/// already have been removed from the virtualizing panel's list of realized containers before
/// this method is called. This method must not be called if
/// <see cref="IsItemItsOwnContainer"/> returned true for the item.
/// <see cref="NeedsContainer(object, int, out object?)"/> returned false for the item.
/// </remarks>
public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container);

58
src/Avalonia.Controls/ItemsControl.cs

@ -307,6 +307,12 @@ namespace Avalonia.Controls
set => SetValue(AreVerticalSnapPointsRegularProperty, value);
}
/// <summary>
/// Gets a default recycle key that can be used when an <see cref="ItemsControl"/> supports
/// a single container type.
/// </summary>
protected static object DefaultRecycleKey { get; } = new object();
/// <summary>
/// Returns the container for the item at the specified index.
/// </summary>
@ -362,7 +368,10 @@ namespace Avalonia.Controls
/// <summary>
/// Creates or a container that can be used to display an item.
/// </summary>
protected internal virtual Control CreateContainerForItemOverride() => new ContentPresenter();
protected internal virtual Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new ContentPresenter();
}
/// <summary>
/// Prepares the specified element to display the specified item.
@ -495,11 +504,52 @@ namespace Avalonia.Controls
}
/// <summary>
/// Determines whether the specified item is (or is eligible to be) its own container.
/// Determines whether the specified item can be its own container.
/// </summary>
/// <param name="item">The item to check.</param>
/// <returns>true if the item is (or is eligible to be) its own container; otherwise, false.</returns>
protected internal virtual bool IsItemItsOwnContainerOverride(Control item) => true;
/// <param name="index">The index of the item.</param>
/// <param name="recycleKey">
/// When the method returns, contains a key that can be used to locate a previously
/// recycled container of the correct type, or null if the item cannot be recycled.
/// If the item is its own container then by definition it cannot be recycled, so
/// <paramref name="recycleKey"/> shoud be set to null.
/// </param>
/// <returns>
/// true if the item needs a container; otherwise false if the item can itself be used
/// as a container.
/// </returns>
protected internal virtual bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<Control>(item, out recycleKey);
}
/// <summary>
/// A default implementation of <see cref="NeedsContainerOverride(object, int, out object?)"/>
/// that returns true and sets the recycle key to <see cref="DefaultRecycleKey"/> if the item
/// is not a <typeparamref name="T"/> .
/// </summary>
/// <typeparam name="T">The container type.</typeparam>
/// <param name="item">The item.</param>
/// <param name="recycleKey">
/// When the method returns, contains <see cref="DefaultRecycleKey"/> if
/// <paramref name="item"/> is not of type <typeparamref name="T"/>; otherwise null.
/// </param>
/// <returns>
/// true if <paramref name="item"/> is of type <typeparamref name="T"/>; otherwise false.
/// </returns>
protected bool NeedsContainer<T>(object? item, out object? recycleKey) where T : Control
{
if (item is T)
{
recycleKey = null;
return false;
}
else
{
recycleKey = DefaultRecycleKey;
return true;
}
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)

11
src/Avalonia.Controls/ListBox.cs

@ -108,8 +108,15 @@ namespace Avalonia.Controls
/// </summary>
public void UnselectAll() => Selection.Clear();
protected internal override Control CreateContainerForItemOverride() => new ListBoxItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ListBoxItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new ListBoxItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<ListBoxItem>(item, out recycleKey);
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)

18
src/Avalonia.Controls/MenuBase.cs

@ -133,8 +133,22 @@ namespace Avalonia.Controls
/// <inheritdoc/>
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
protected internal override Control CreateContainerForItemOverride() => new MenuItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new MenuItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
if (item is MenuItem or Separator)
{
recycleKey = null;
return false;
}
recycleKey = DefaultRecycleKey;
return true;
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)

18
src/Avalonia.Controls/MenuItem.cs

@ -339,8 +339,22 @@ namespace Avalonia.Controls
/// <inheritdoc/>
void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent));
protected internal override Control CreateContainerForItemOverride() => new MenuItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is MenuItem or Separator;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new MenuItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
if (item is MenuItem or Separator)
{
recycleKey = null;
return false;
}
recycleKey = DefaultRecycleKey;
return true;
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{

8
src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs

@ -113,14 +113,14 @@ namespace Avalonia.Controls.Presenters
var generator = itemsControl.ItemContainerGenerator;
Control container;
if (item is Control c && generator.IsItemItsOwnContainer(c))
if (generator.NeedsContainer(item, index, out var recycleKey))
{
container = c;
container.SetValue(ItemIsOwnContainerProperty, true);
container = generator.CreateContainer(item, index, recycleKey);
}
else
{
container = generator.CreateContainer();
container = (Control)item!;
container.SetValue(ItemIsOwnContainerProperty, true);
}
generator.PrepareItemContainer(container, item, index);

11
src/Avalonia.Controls/Primitives/TabStrip.cs

@ -16,8 +16,15 @@ namespace Avalonia.Controls.Primitives
ItemsPanelProperty.OverrideDefaultValue<TabStrip>(DefaultPanel);
}
protected internal override Control CreateContainerForItemOverride() => new TabStripItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabStripItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new TabStripItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<TabStripItem>(item, out recycleKey);
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)

11
src/Avalonia.Controls/TabControl.cs

@ -148,8 +148,15 @@ namespace Avalonia.Controls
return RegisterContentPresenter(presenter);
}
protected internal override Control CreateContainerForItemOverride() => new TabItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TabItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new TabItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<TabItem>(item, out recycleKey);
}
protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{

11
src/Avalonia.Controls/TreeView.cs

@ -486,8 +486,15 @@ namespace Avalonia.Controls
return (false, null);
}
protected internal override Control CreateContainerForItemOverride() => new TreeViewItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TreeViewItem;
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new TreeViewItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<TreeViewItem>(item, out recycleKey);
}
protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index)
{

8
src/Avalonia.Controls/TreeViewItem.cs

@ -91,14 +91,14 @@ namespace Avalonia.Controls
internal TreeView? TreeViewOwner => _treeView;
protected internal override Control CreateContainerForItemOverride()
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return EnsureTreeView().CreateContainerForItemOverride();
return EnsureTreeView().CreateContainerForItemOverride(item, index, recycleKey);
}
protected internal override bool IsItemItsOwnContainerOverride(Control item)
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return EnsureTreeView().IsItemItsOwnContainerOverride(item);
return EnsureTreeView().NeedsContainerOverride(item, index, out recycleKey);
}
protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index)

97
src/Avalonia.Controls/VirtualizingCarouselPanel.cs

@ -15,13 +15,14 @@ namespace Avalonia.Controls
/// </summary>
public class VirtualizingCarouselPanel : VirtualizingPanel, ILogicalScrollable
{
private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
AvaloniaProperty.RegisterAttached<VirtualizingCarouselPanel, Control, bool>("ItemIsOwnContainer");
private static readonly AttachedProperty<object?> RecycleKeyProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, object?>("RecycleKey");
private static readonly object s_itemIsItsOwnContainer = new object();
private Size _extent;
private Vector _offset;
private Size _viewport;
private Stack<Control>? _recyclePool;
private Dictionary<object, Stack<Control>>? _recyclePool;
private Control? _realized;
private int _realizedIndex = -1;
private Control? _transitionFrom;
@ -172,7 +173,7 @@ namespace Avalonia.Controls
return null;
if (index == _realizedIndex)
return _realized;
if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty))
if (Items[index] is Control c && c.GetValue(RecycleKeyProperty) == s_itemIsItsOwnContainer)
return c;
return null;
}
@ -246,10 +247,27 @@ namespace Avalonia.Controls
private Control GetOrCreateElement(IReadOnlyList<object?> items, int index)
{
return GetRealizedElement(index) ??
GetItemIsOwnContainer(items, index) ??
GetRecycledElement(items, index) ??
CreateElement(items, index);
Debug.Assert(ItemContainerGenerator is not null);
var e = GetRealizedElement(index);
if (e is null)
{
var item = items[index];
var generator = ItemContainerGenerator!;
if (generator.NeedsContainer(item, index, out var recycleKey))
{
e = GetRecycledElement(item, index, recycleKey) ??
CreateElement(item, index, recycleKey);
}
else
{
e = GetItemAsOwnContainer(item, index);
}
}
return e;
}
private Control? GetRealizedElement(int index)
@ -257,44 +275,37 @@ namespace Avalonia.Controls
return _realizedIndex == index ? _realized : null;
}
private Control? GetItemIsOwnContainer(IReadOnlyList<object?> items, int index)
private Control GetItemAsOwnContainer(object? item, int index)
{
Debug.Assert(ItemContainerGenerator is not null);
var item = items[index];
var controlItem = (Control)item!;
var generator = ItemContainerGenerator!;
if (item is Control controlItem)
if (!controlItem.IsSet(RecycleKeyProperty))
{
var generator = ItemContainerGenerator;
if (controlItem.IsSet(ItemIsOwnContainerProperty))
{
controlItem.IsVisible = true;
return controlItem;
}
else if (generator.IsItemItsOwnContainer(controlItem))
{
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(ItemIsOwnContainerProperty, true);
generator.ItemContainerPrepared(controlItem, item, index);
return controlItem;
}
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(RecycleKeyProperty, s_itemIsItsOwnContainer);
generator.ItemContainerPrepared(controlItem, item, index);
}
return null;
controlItem.IsVisible = true;
return controlItem;
}
private Control? GetRecycledElement(IReadOnlyList<object?> items, int index)
private Control? GetRecycledElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator;
var item = items[index];
if (recycleKey is null)
return null;
var generator = ItemContainerGenerator!;
if (_recyclePool?.Count > 0)
if (_recyclePool?.TryGetValue(recycleKey, out var recyclePool) == true && recyclePool.Count > 0)
{
var recycled = _recyclePool.Pop();
var recycled = recyclePool.Pop();
recycled.IsVisible = true;
generator.PrepareItemContainer(recycled, item, index);
generator.ItemContainerPrepared(recycled, item, index);
@ -304,14 +315,14 @@ namespace Avalonia.Controls
return null;
}
private Control CreateElement(IReadOnlyList<object?> items, int index)
private Control CreateElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator;
var item = items[index];
var container = generator.CreateContainer();
var generator = ItemContainerGenerator!;
var container = generator.CreateContainer(item, index, recycleKey);
container.SetValue(RecycleKeyProperty, recycleKey);
generator.PrepareItemContainer(container, item, index);
AddInternalChild(container);
generator.ItemContainerPrepared(container, item, index);
@ -323,7 +334,10 @@ namespace Avalonia.Controls
{
Debug.Assert(ItemContainerGenerator is not null);
if (element.IsSet(ItemIsOwnContainerProperty))
var recycleKey = element.GetValue(RecycleKeyProperty);
Debug.Assert(recycleKey is not null);
if (recycleKey == s_itemIsItsOwnContainer)
{
element.IsVisible = false;
}
@ -331,7 +345,14 @@ namespace Avalonia.Controls
{
ItemContainerGenerator.ClearItemContainer(element);
_recyclePool ??= new();
_recyclePool.Push(element);
if (!_recyclePool.TryGetValue(recycleKey, out var pool))
{
pool = new();
_recyclePool.Add(recycleKey, pool);
}
pool.Push(element);
element.IsVisible = false;
}
}

112
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -52,10 +52,11 @@ namespace Avalonia.Controls
nameof(VerticalSnapPointsChanged),
RoutingStrategies.Bubble);
private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, bool>("ItemIsOwnContainer");
private static readonly AttachedProperty<object?> RecycleKeyProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, object?>("RecycleKey");
private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0);
private static readonly object s_itemIsItsOwnContainer = new object();
private readonly Action<Control, int> _recycleElement;
private readonly Action<Control> _recycleElementOnItemRemoved;
private readonly Action<Control, int, int> _updateElementIndex;
@ -68,7 +69,7 @@ namespace Avalonia.Controls
private RealizedStackElements? _realizedElements;
private ScrollViewer? _scrollViewer;
private Rect _viewport = s_invalidViewport;
private Stack<Control>? _recyclePool;
private Dictionary<object, Stack<Control>>? _recyclePool;
private Control? _unrealizedFocusedElement;
private int _unrealizedFocusedIndex = -1;
@ -331,7 +332,7 @@ namespace Avalonia.Controls
return null;
if (_realizedElements?.GetElement(index) is { } realized)
return realized;
if (Items[index] is Control c && c.GetValue(ItemIsOwnContainerProperty))
if (Items[index] is Control c && c.GetValue(RecycleKeyProperty) == s_itemIsItsOwnContainer)
return c;
return null;
}
@ -492,8 +493,7 @@ namespace Avalonia.Controls
c = c?.GetVisualParent();
}
return viewport;
return viewport.Intersect(new Rect(0, 0, double.PositiveInfinity, double.PositiveInfinity));
}
private void RealizeElements(
@ -561,10 +561,26 @@ namespace Avalonia.Controls
private Control GetOrCreateElement(IReadOnlyList<object?> items, int index)
{
var e = GetRealizedElement(index) ??
GetItemIsOwnContainer(items, index) ??
GetRecycledElement(items, index) ??
CreateElement(items, index);
Debug.Assert(ItemContainerGenerator is not null);
var e = GetRealizedElement(index);
if (e is null)
{
var item = items[index];
var generator = ItemContainerGenerator!;
if (generator.NeedsContainer(item, index, out var recycleKey))
{
e = GetRecycledElement(item, index, recycleKey) ??
CreateElement(item, index, recycleKey);
}
else
{
e = GetItemAsOwnContainer(item, index);
}
}
return e;
}
@ -575,38 +591,33 @@ namespace Avalonia.Controls
return _realizedElements?.GetElement(index);
}
private Control? GetItemIsOwnContainer(IReadOnlyList<object?> items, int index)
private Control GetItemAsOwnContainer(object? item, int index)
{
var item = items[index];
Debug.Assert(ItemContainerGenerator is not null);
if (item is Control controlItem)
{
var generator = ItemContainerGenerator!;
var controlItem = (Control)item!;
var generator = ItemContainerGenerator!;
if (controlItem.IsSet(ItemIsOwnContainerProperty))
{
controlItem.IsVisible = true;
return controlItem;
}
else if (generator.IsItemItsOwnContainer(controlItem))
{
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(ItemIsOwnContainerProperty, true);
generator.ItemContainerPrepared(controlItem, item, index);
return controlItem;
}
if (!controlItem.IsSet(RecycleKeyProperty))
{
generator.PrepareItemContainer(controlItem, controlItem, index);
AddInternalChild(controlItem);
controlItem.SetValue(RecycleKeyProperty, s_itemIsItsOwnContainer);
generator.ItemContainerPrepared(controlItem, item, index);
}
return null;
controlItem.IsVisible = true;
return controlItem;
}
private Control? GetRecycledElement(IReadOnlyList<object?> items, int index)
private Control? GetRecycledElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
if (recycleKey is null)
return null;
var generator = ItemContainerGenerator!;
var item = items[index];
if (_unrealizedFocusedIndex == index && _unrealizedFocusedElement is not null)
{
@ -617,9 +628,9 @@ namespace Avalonia.Controls
return element;
}
if (_recyclePool?.Count > 0)
if (_recyclePool?.TryGetValue(recycleKey, out var recyclePool) == true && recyclePool.Count > 0)
{
var recycled = _recyclePool.Pop();
var recycled = recyclePool.Pop();
recycled.IsVisible = true;
generator.PrepareItemContainer(recycled, item, index);
generator.ItemContainerPrepared(recycled, item, index);
@ -629,14 +640,14 @@ namespace Avalonia.Controls
return null;
}
private Control CreateElement(IReadOnlyList<object?> items, int index)
private Control CreateElement(object? item, int index, object? recycleKey)
{
Debug.Assert(ItemContainerGenerator is not null);
var generator = ItemContainerGenerator!;
var item = items[index];
var container = generator.CreateContainer();
var container = generator.CreateContainer(item, index, recycleKey);
container.SetValue(RecycleKeyProperty, recycleKey);
generator.PrepareItemContainer(container, item, index);
AddInternalChild(container);
generator.ItemContainerPrepared(container, item, index);
@ -650,7 +661,10 @@ namespace Avalonia.Controls
_scrollViewer?.UnregisterAnchorCandidate(element);
if (element.IsSet(ItemIsOwnContainerProperty))
var recycleKey = element.GetValue(RecycleKeyProperty);
Debug.Assert(recycleKey is not null);
if (recycleKey == s_itemIsItsOwnContainer)
{
element.IsVisible = false;
}
@ -663,8 +677,7 @@ namespace Avalonia.Controls
else
{
ItemContainerGenerator!.ClearItemContainer(element);
_recyclePool ??= new();
_recyclePool.Push(element);
PushToRecyclePool(recycleKey, element);
element.IsVisible = false;
}
}
@ -673,19 +686,34 @@ namespace Avalonia.Controls
{
Debug.Assert(ItemContainerGenerator is not null);
if (element.IsSet(ItemIsOwnContainerProperty))
var recycleKey = element.GetValue(RecycleKeyProperty);
Debug.Assert(recycleKey is not null);
if (recycleKey == s_itemIsItsOwnContainer)
{
RemoveInternalChild(element);
}
else
{
ItemContainerGenerator!.ClearItemContainer(element);
_recyclePool ??= new();
_recyclePool.Push(element);
PushToRecyclePool(recycleKey, element);
element.IsVisible = false;
}
}
private void PushToRecyclePool(object recycleKey, Control element)
{
_recyclePool ??= new();
if (!_recyclePool.TryGetValue(recycleKey, out var pool))
{
pool = new();
_recyclePool.Add(recycleKey, pool);
}
pool.Push(element);
}
private void UpdateElementIndex(Control element, int oldIndex, int newIndex)
{
Debug.Assert(ItemContainerGenerator is not null);

21
src/Browser/Avalonia.Browser/BrowserAppBuilder.cs

@ -11,6 +11,27 @@ public class BrowserPlatformOptions
/// If null, default path resolved depending on the backend (browser or blazor) is used.
/// </summary>
public Func<string, string>? FrameworkAssetPathResolver { get; set; }
/// <summary>
/// Defines if the service worker used by Avalonia should be registered.
/// If registered, service worker can work as a save file picker fallback on the browsers that don't support native implementation.
/// For more details, see https://github.com/jimmywarting/native-file-system-adapter#a-note-when-downloading-with-the-polyfilled-version.
/// </summary>
public bool RegisterAvaloniaServiceWorker { get; set; }
/// <summary>
/// If <see cref="RegisterAvaloniaServiceWorker"/> is enabled, it is possible to redefine scope for the worker.
/// By default, current domain root is used as a scope.
/// </summary>
public string? AvaloniaServiceWorkerScope { get; set; }
/// <summary>
/// Avalonia uses "native-file-system-adapter" polyfill for the file dialogs.
/// If native implementation is available, by default it is used.
/// This property forces polyfill to be always used.
/// For more details, see https://github.com/jimmywarting/native-file-system-adapter#a-note-when-downloading-with-the-polyfilled-version.
/// </summary>
public bool PreferFileDialogPolyfill { get; set; }
}
public static class BrowserAppBuilder

9
src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs

@ -25,6 +25,15 @@ internal static partial class AvaloniaModule
public static Task ImportStorage() => s_importStorage.Value;
public static string ResolveServiceWorkerPath()
{
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return options.FrameworkAssetPathResolver!("sw.js");
}
[JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)]
public static partial bool IsMobile();
[JSImport("registerServiceWorker", AvaloniaModule.MainModuleName)]
public static partial void RegisterServiceWorker(string path, string? scope);
}

6
src/Browser/Avalonia.Browser/Interop/StorageHelper.cs

@ -9,15 +9,15 @@ internal static partial class StorageHelper
public static partial bool HasNativeFilePicker();
[JSImport("StorageProvider.selectFolderDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn);
public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn, bool preferPolyfill);
[JSImport("StorageProvider.openFileDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> OpenFileDialog(JSObject? startIn, bool multiple,
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption);
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption, bool preferPolyfill);
[JSImport("StorageProvider.saveFileDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> SaveFileDialog(JSObject? startIn, string? suggestedName,
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption);
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption, bool preferPolyfill);
[JSImport("StorageItem.createWellKnownDirectory", AvaloniaModule.StorageModuleName)]
public static partial JSObject CreateWellKnownDirectory(string wellKnownDirectory);

16
src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs

@ -6,20 +6,22 @@ using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
using Avalonia.Browser.Interop;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Browser.Storage;
internal record FilePickerAcceptType(string Description, IReadOnlyDictionary<string, IReadOnlyList<string>> Accept);
internal class BrowserStorageProvider : IStorageProvider
{
internal const string PickerCancelMessage = "The user aborted a request";
internal const string NoPermissionsMessage = "Permissions denied";
public bool CanOpen => true;
public bool CanSave => StorageHelper.HasNativeFilePicker();
public bool CanSave => true;
public bool CanPickFolder => true;
private bool PreferPolyfill =>
AvaloniaLocator.Current.GetService<BrowserPlatformOptions>()?.PreferFileDialogPolyfill ?? false;
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
await AvaloniaModule.ImportStorage();
@ -29,7 +31,7 @@ internal class BrowserStorageProvider : IStorageProvider
try
{
using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, excludeAll);
using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, excludeAll, PreferPolyfill);
if (items is null)
{
return Array.Empty<IStorageFile>();
@ -63,7 +65,9 @@ internal class BrowserStorageProvider : IStorageProvider
try
{
var item = await StorageHelper.SaveFileDialog(startIn, options.SuggestedFileName, types, excludeAll);
var suggestedName =
StorageProviderHelpers.NameWithExtension(options.SuggestedFileName, options.DefaultExtension, null);
var item = await StorageHelper.SaveFileDialog(startIn, suggestedName, types, excludeAll, PreferPolyfill);
return item is not null ? new JSStorageFile(item) : null;
}
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))
@ -89,7 +93,7 @@ internal class BrowserStorageProvider : IStorageProvider
try
{
var item = await StorageHelper.SelectFolderDialog(startIn);
var item = await StorageHelper.SelectFolderDialog(startIn, PreferPolyfill);
return item is not null ? new[] { new JSStorageFolder(item) } : Array.Empty<IStorageFolder>();
}
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))

8
src/Browser/Avalonia.Browser/WindowingPlatform.cs

@ -1,5 +1,6 @@
using System;
using System.Threading;
using Avalonia.Browser.Interop;
using Avalonia.Browser.Skia;
using Avalonia.Input;
using Avalonia.Input.Platform;
@ -46,6 +47,13 @@ namespace Avalonia.Browser
.Bind<IPlatformGraphics>().ToConstant(new BrowserSkiaGraphics())
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
if (AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() is { } options
&& options.RegisterAvaloniaServiceWorker)
{
var swPath = AvaloniaModule.ResolveServiceWorkerPath();
AvaloniaModule.RegisterServiceWorker(swPath, options.AvaloniaServiceWorkerScope);
}
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)

3
src/Browser/Avalonia.Browser/webapp/build.js

@ -1,7 +1,8 @@
require("esbuild").build({
entryPoints: [
"./modules/avalonia.ts",
"./modules/storage.ts"
"./modules/storage.ts",
"./modules/sw.ts"
],
outdir: "../wwwroot",
bundle: true,

9
src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts

@ -7,6 +7,12 @@ import { NativeControlHost } from "./avalonia/nativeControlHost";
import { NavigationHelper } from "./avalonia/navigationHelper";
import { GeneralHelpers } from "./avalonia/generalHelpers";
async function registerServiceWorker(path: string, scope: string | undefined) {
if ("serviceWorker" in navigator) {
await globalThis.navigator.serviceWorker.register(path, scope ? { scope } : undefined);
}
}
export {
Caniuse,
Canvas,
@ -17,5 +23,6 @@ export {
StreamHelper,
NativeControlHost,
NavigationHelper,
GeneralHelpers
GeneralHelpers,
registerServiceWorker
};

7
src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts

@ -18,12 +18,7 @@ export class StreamHelper {
const array = new Uint8Array(span.byteLength);
span.copyTo(array);
const data = {
type: "write",
data: array
};
return await stream.write(data);
return await stream.write(array);
}
public static byteLength(stream: Blob) {

23
src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts

@ -1,6 +1,6 @@
import { avaloniaDb, fileBookmarksStore } from "./indexedDb";
import { StorageItem, StorageItems } from "./storageItem";
import { showOpenFilePicker, showDirectoryPicker, FileSystemFileHandle } from "native-file-system-adapter";
import { showOpenFilePicker, showDirectoryPicker, showSaveFilePicker, FileSystemFileHandle } from "native-file-system-adapter";
declare global {
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
@ -12,10 +12,12 @@ declare global {
export class StorageProvider {
public static async selectFolderDialog(
startIn: StorageItem | null): Promise<StorageItem> {
startIn: StorageItem | null,
preferPolyfill: boolean): Promise<StorageItem> {
// 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined.
const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined)
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
_preferPolyfill: preferPolyfill
};
const handle = await showDirectoryPicker(options as any);
@ -24,12 +26,14 @@ export class StorageProvider {
public static async openFileDialog(
startIn: StorageItem | null, multiple: boolean,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItems> {
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean,
preferPolyfill: boolean): Promise<StorageItems> {
const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
multiple,
excludeAcceptAllOption,
types: (types ?? undefined)
types: (types ?? undefined),
_preferPolyfill: preferPolyfill
};
const handles = await showOpenFilePicker(options);
@ -38,16 +42,17 @@ export class StorageProvider {
public static async saveFileDialog(
startIn: StorageItem | null, suggestedName: string | null,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItem> {
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean,
preferPolyfill: boolean): Promise<StorageItem> {
const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
suggestedName: (suggestedName ?? undefined),
excludeAcceptAllOption,
types: (types ?? undefined)
types: (types ?? undefined),
_preferPolyfill: preferPolyfill
};
// Always prefer native save file picker, as polyfill solutions are not reliable.
const handle = await (globalThis as any).showSaveFilePicker(options);
const handle = await showSaveFilePicker(options);
return StorageItem.createFromHandle(handle);
}

78
src/Browser/Avalonia.Browser/webapp/modules/sw.ts

@ -0,0 +1,78 @@
const WRITE = 0;
const PULL = 0;
const ERROR = 1;
const ABORT = 1;
const CLOSE = 2;
class MessagePortSource implements UnderlyingSource {
private controller?: ReadableStreamController<any>;
constructor (private readonly port: MessagePort) {
this.port.onmessage = evt => this.onMessage(evt.data);
}
start (controller: ReadableStreamController<any>) {
this.controller = controller;
}
cancel (reason: Error) {
// Firefox can notify a cancel event, chrome can't
// https://bugs.chromium.org/p/chromium/issues/detail?id=638494
this.port.postMessage({ type: ERROR, reason: reason.message });
this.port.close();
}
onMessage (message: { type: number; chunk: Uint8Array; reason: any }) {
// enqueue() will call pull() if needed when there's no backpressure
if (!this.controller) {
return;
}
if (message.type === WRITE) {
this.controller.enqueue(message.chunk);
this.port.postMessage({ type: PULL });
}
if (message.type === ABORT) {
this.controller.error(message.reason);
this.port.close();
}
if (message.type === CLOSE) {
this.controller.close();
this.port.close();
}
}
}
self.addEventListener("install", () => {
(self as any).skipWaiting();
});
self.addEventListener("activate", event /* ExtendableEvent */ => {
(event as any).waitUntil((self as any).clients.claim());
});
const map = new Map();
// This should be called once per download
// Each event has a dataChannel that the data will be piped through
globalThis.addEventListener("message", evt => {
const data = evt.data;
if (data.url && data.readablePort) {
data.rs = new ReadableStream(
new MessagePortSource(evt.data.readablePort),
new CountQueuingStrategy({ highWaterMark: 4 })
);
map.set(data.url, data);
}
});
globalThis.addEventListener("fetch", evt => {
const url = (evt as any).request.url;
const data = map.get(url);
if (!data) return null;
map.delete(url);
(evt as any).respondWith(new Response(data.rs, {
headers: data.headers
}));
});
export {};

50
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs

@ -36,13 +36,29 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined");
}
if (valueNode.Manipulation is not XamlObjectInitializationNode
{
Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty
})
if (valueNode.Manipulation is not XamlObjectInitializationNode initializationNode)
{
throw new XamlDocumentParseException(context.CurrentDocument,
$"Source property must be set on the \"{nodeTypeName}\" node.", valueNode);
$"Invalid \"{nodeTypeName}\" node initialization.", valueNode);
}
var additionalProperties = new List<IXamlAstManipulationNode>();
if (initializationNode.Manipulation is not XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty)
{
if (initializationNode.Manipulation is XamlManipulationGroupNode manipulationGroup
&& manipulationGroup.Children.OfType<XamlPropertyAssignmentNode>()
.FirstOrDefault(p => p.Property.Name == "Source") is { } sourceProperty2)
{
sourceProperty = sourceProperty2;
// We need to copy some additional properties from ResourceInclude to ResourceDictionary except the Source one.
// If there is any missing properties, then XAML compiler will throw an error in the emitter code.
additionalProperties = manipulationGroup.Children.Where(c => c != sourceProperty2).ToList();
}
else
{
throw new XamlDocumentParseException(context.CurrentDocument,
$"Source property must be set on the \"{nodeTypeName}\" node.", valueNode);
}
}
var (assetPathUri, sourceUriNode) = ResolveSourceFromXamlInclude(context, nodeTypeName, sourceProperty, false);
@ -65,12 +81,12 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
{
if (targetDocument.BuildMethod is not null)
{
return FromMethod(context, targetDocument.BuildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
return FromMethod(context, targetDocument.BuildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
if (targetDocument.ClassType is not null)
{
return FromType(context, targetDocument.ClassType, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
return FromType(context, targetDocument.ClassType, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
return context.ParseError(
@ -95,11 +111,11 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
var buildMethod = avaResType.FindMethod(m => m.Name == relativeName);
if (buildMethod is not null)
{
return FromMethod(context, buildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
return FromMethod(context, buildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
else if (assetAssembly.FindType(fullTypeName) is { } type)
{
return FromType(context, type, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
return FromType(context, type, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
return context.ParseError(
@ -108,7 +124,8 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
}
private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlAstNode li,
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly)
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly,
IEnumerable<IXamlAstManipulationNode> manipulationNodes)
{
if (!expectedLoadedType.IsAssignableFrom(type))
{
@ -116,15 +133,17 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
$"Resource \"{assetPathUri}\" is defined as \"{type}\" type in the \"{assembly}\" assembly, but expected \"{expectedLoadedType}\".",
li, fallbackNode);
}
IXamlAstNode newObjNode = new XamlAstObjectNode(li, new XamlAstClrTypeReference(li, type, false));
((XamlAstObjectNode)newObjNode).Children.AddRange(manipulationNodes);
newObjNode = new AvaloniaXamlIlConstructorServiceProviderTransformer().Transform(context, newObjNode);
newObjNode = new ConstructableObjectTransformer().Transform(context, newObjNode);
return new NewObjectTransformer().Transform(context, newObjNode);
}
private static IXamlAstNode FromMethod(AstTransformationContext context, IXamlMethod method, IXamlAstNode li,
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly)
IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly,
IEnumerable<IXamlAstManipulationNode> manipulationNodes)
{
if (!expectedLoadedType.IsAssignableFrom(method.ReturnType))
{
@ -134,8 +153,11 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
}
var sp = context.Configuration.TypeMappings.ServiceProvider;
return new XamlStaticOrTargetedReturnMethodCallNode(li, method,
new[] { new NewServiceProviderNode(sp, li) });
return new XamlValueWithManipulationNode(li,
new XamlStaticOrTargetedReturnMethodCallNode(li, method,
new[] { new NewServiceProviderNode(sp, li) }),
new XamlManipulationGroupNode(li, manipulationNodes));
}
internal static (string?, IXamlAstNode?) ResolveSourceFromXamlInclude(

10
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs

@ -10,7 +10,8 @@ internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransform
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
var type = context.GetAvaloniaTypes().IThemeVariantProvider;
var avTypes = context.GetAvaloniaTypes();
var type = avTypes.IThemeVariantProvider;
if (!(node is XamlAstObjectNode on
&& type.IsAssignableFrom(on.Type.GetClrType())))
return node;
@ -21,6 +22,13 @@ internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransform
if (keyDirective is null)
return node;
var themeDictionariesColl = avTypes.IDictionaryT.MakeGenericType(avTypes.ThemeVariant, avTypes.IThemeVariantProvider);
if (context.ParentNodes().FirstOrDefault() is not XamlAstXamlPropertyValueNode propertyValueNode
|| !themeDictionariesColl.IsAssignableFrom(propertyValueNode.Property.GetClrProperty().Getter.ReturnType))
{
return node;
}
var keyProp = type.Properties.First(p => p.Name == "Key");
on.Children.Add(new XamlAstXamlPropertyValueNode(keyDirective,
new XamlAstClrProperty(keyDirective, keyProp, context.Configuration),

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -65,6 +65,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType Int { get; }
public IXamlType Long { get; }
public IXamlType Uri { get; }
public IXamlType IDictionaryT { get; }
public IXamlType FontFamily { get; }
public IXamlConstructor FontFamilyConstructorUriName { get; }
public IXamlType Thickness { get; }
@ -194,6 +195,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
Int = cfg.TypeSystem.GetType("System.Int32");
Long = cfg.TypeSystem.GetType("System.Int64");
Uri = cfg.TypeSystem.GetType("System.Uri");
IDictionaryT = cfg.TypeSystem.GetType("System.Collections.Generic.IDictionary`2");
FontFamily = cfg.TypeSystem.GetType("Avalonia.Media.FontFamily");
FontFamilyConstructorUriName = FontFamily.GetConstructor(new List<IXamlType> { Uri, XamlIlTypes.String });
ThemeVariant = cfg.TypeSystem.GetType("Avalonia.Styling.ThemeVariant");

240
tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs

@ -26,6 +26,138 @@ namespace Avalonia.Base.UnitTests.Input
Assert.Same(target, FocusManager.Instance.Current);
}
}
[Fact]
public void Invisible_Controls_Should_Not_Receive_Focus()
{
Button target;
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var root = new TestRoot
{
Child = target = new Button() { IsVisible = false}
};
Assert.Null(FocusManager.Instance.Current);
target.Focus();
Assert.False(target.IsFocused);
Assert.False(target.IsKeyboardFocusWithin);
Assert.Null(FocusManager.Instance.Current);
}
}
[Fact]
public void Effectively_Invisible_Controls_Should_Not_Receive_Focus()
{
var target = new Button();
Panel container;
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var root = new TestRoot
{
Child = container = new Panel
{
IsVisible = false,
Children = { target }
}
};
Assert.Null(FocusManager.Instance.Current);
target.Focus();
Assert.False(target.IsFocused);
Assert.False(target.IsKeyboardFocusWithin);
Assert.Null(FocusManager.Instance.Current);
}
}
[Fact]
public void Trying_To_Focus_Invisible_Control_Should_Not_Change_Focus()
{
Button first;
Button second;
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var root = new TestRoot
{
Child = new StackPanel
{
Children =
{
(first = new Button()),
(second = new Button() { IsVisible = false}),
}
}
};
first.Focus();
Assert.Same(first, FocusManager.Instance.Current);
second.Focus();
Assert.Same(first, FocusManager.Instance.Current);
}
}
[Fact]
public void Disabled_Controls_Should_Not_Receive_Focus()
{
Button target;
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var root = new TestRoot
{
Child = target = new Button() { IsEnabled = false }
};
Assert.Null(FocusManager.Instance.Current);
target.Focus();
Assert.False(target.IsFocused);
Assert.False(target.IsKeyboardFocusWithin);
Assert.Null(FocusManager.Instance.Current);
}
}
[Fact]
public void Effectively_Disabled_Controls_Should_Not_Receive_Focus()
{
var target = new Button();
Panel container;
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var root = new TestRoot
{
Child = container = new Panel
{
IsEnabled = false,
Children = { target }
}
};
Assert.Null(FocusManager.Instance.Current);
target.Focus();
Assert.False(target.IsFocused);
Assert.False(target.IsKeyboardFocusWithin);
Assert.Null(FocusManager.Instance.Current);
}
}
[Fact]
public void Focus_Should_Not_Get_Restored_To_Enabled_Control()
@ -54,6 +186,90 @@ namespace Avalonia.Base.UnitTests.Input
}
}
[Fact]
public void Focus_Should_Be_Cleared_When_Control_Is_Hidden()
{
Button target;
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var root = new TestRoot
{
Child = target = new Button()
};
target.Focus();
target.IsVisible = false;
Assert.Null(FocusManager.Instance.Current);
}
}
[Fact(Skip = "Need to implement IsEffectivelyVisible change notifications.")]
public void Focus_Should_Be_Cleared_When_Control_Is_Effectively_Hidden()
{
Border container;
Button target;
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var root = new TestRoot
{
Child = container = new Border
{
Child = target = new Button(),
}
};
target.Focus();
container.IsVisible = false;
Assert.Null(FocusManager.Instance.Current);
}
}
[Fact]
public void Focus_Should_Be_Cleared_When_Control_Is_Disabled()
{
Button target;
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var root = new TestRoot
{
Child = target = new Button()
};
target.Focus();
target.IsEnabled = false;
Assert.Null(FocusManager.Instance.Current);
}
}
[Fact]
public void Focus_Should_Be_Cleared_When_Control_Is_Effectively_Disabled()
{
Border container;
Button target;
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var root = new TestRoot
{
Child = container = new Border
{
Child = target = new Button(),
}
};
target.Focus();
container.IsEnabled = false;
Assert.Null(FocusManager.Instance.Current);
}
}
[Fact]
public void Focus_Should_Be_Cleared_When_Control_Is_Removed_From_VisualTree()
{
@ -78,8 +294,8 @@ namespace Avalonia.Base.UnitTests.Input
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Decorator();
var target2 = new Decorator();
var target1 = new Decorator { Focusable = true };
var target2 = new Decorator { Focusable = true };
var root = new TestRoot
{
Child = new StackPanel
@ -115,8 +331,8 @@ namespace Avalonia.Base.UnitTests.Input
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Decorator();
var target2 = new Decorator();
var target1 = new Decorator { Focusable = true };
var target2 = new Decorator { Focusable = true };
var root = new TestRoot
{
Child = new StackPanel
@ -157,8 +373,8 @@ namespace Avalonia.Base.UnitTests.Input
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Decorator();
var target2 = new Decorator();
var target1 = new Decorator { Focusable = true };
var target2 = new Decorator { Focusable = true };
var root = new TestRoot
{
Child = new StackPanel
@ -190,8 +406,8 @@ namespace Avalonia.Base.UnitTests.Input
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Decorator();
var target2 = new Decorator();
var target1 = new Decorator { Focusable = true };
var target2 = new Decorator { Focusable = true };
var panel1 = new Panel { Children = { target1 } };
var panel2 = new Panel { Children = { target2 } };
var root = new TestRoot
@ -245,8 +461,8 @@ namespace Avalonia.Base.UnitTests.Input
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Decorator();
var target2 = new Decorator();
var target1 = new Decorator { Focusable = true };
var target2 = new Decorator { Focusable = true };
var root = new TestRoot
{
Child = new StackPanel
@ -290,8 +506,8 @@ namespace Avalonia.Base.UnitTests.Input
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var target1 = new Decorator();
var target2 = new Decorator();
var target1 = new Decorator { Focusable = true };
var target2 = new Decorator { Focusable = true };
var root1 = new TestRoot
{

6
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -1034,14 +1034,14 @@ namespace Avalonia.Controls.UnitTests
{
Type IStyleable.StyleKey => typeof(ItemsControl);
protected internal override Control CreateContainerForItemOverride()
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new ContainerControl();
}
protected internal override bool IsItemItsOwnContainerOverride(Control item)
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return item is ContainerControl;
return NeedsContainer<ContainerControl>(item, out recycleKey);
}
}

1
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

@ -657,7 +657,6 @@ namespace Avalonia.Controls.UnitTests
{
Template = CreateTemplate(),
Text = "1234",
IsVisible = false
};
var root = new TestRoot { Child = target1 };

16
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -1064,6 +1064,14 @@ namespace Avalonia.Controls.UnitTests.Primitives
// Scroll selected item back into view.
scroll.Offset = new(0, 0);
target.PropertyChanged += (s, e) =>
{
if (e.Property == SelectingItemsControl.SelectedIndexProperty)
{
}
};
Layout(target);
// The selection should be preserved.
@ -1387,14 +1395,14 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
Type IStyleable.StyleKey => typeof(TestSelector);
protected internal override bool IsItemItsOwnContainerOverride(Control item)
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return item is TestContainer;
return new TestContainer();
}
protected internal override Control CreateContainerForItemOverride()
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return new TestContainer();
return NeedsContainer<TestContainer>(item, out recycleKey);
}
}

1
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -594,7 +594,6 @@ namespace Avalonia.Controls.UnitTests
{
Template = CreateTemplate(),
Text = "1234",
IsVisible = false
};
var root = new TestRoot { Child = target1 };

5
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -1761,7 +1761,10 @@ namespace Avalonia.Controls.UnitTests
private class DerivedTreeViewWithDerivedTreeViewItems : TreeView
{
protected internal override Control CreateContainerForItemOverride() => new DerivedTreeViewItem();
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new DerivedTreeViewItem();
}
}
private class DerivedTreeViewItem : TreeViewItem

51
tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

@ -298,7 +298,9 @@ namespace Avalonia.Controls.UnitTests
using var app = App();
var (target, scroll, itemsControl) = CreateTarget();
target.GetRealizedElements().First()!.Focus();
var focused = target.GetRealizedElements().First()!;
focused.Focusable = true;
focused.Focus();
Assert.True(target.GetRealizedElements().First()!.IsKeyboardFocusWithin);
scroll.Offset = new Vector(0, 200);
@ -314,6 +316,7 @@ namespace Avalonia.Controls.UnitTests
var (target, scroll, itemsControl) = CreateTarget();
var focused = target.GetRealizedElements().First()!;
focused.Focusable = true;
focused.Focus();
Assert.True(focused.IsKeyboardFocusWithin);
@ -331,6 +334,7 @@ namespace Avalonia.Controls.UnitTests
var (target, scroll, itemsControl) = CreateTarget();
var focused = target.GetRealizedElements().First()!;
focused.Focusable = true;
focused.Focus();
Assert.True(focused.IsKeyboardFocusWithin);
@ -350,12 +354,14 @@ namespace Avalonia.Controls.UnitTests
var (target, scroll, itemsControl) = CreateTarget();
var originalFocused = target.GetRealizedElements().First()!;
originalFocused.Focusable = true;
originalFocused.Focus();
scroll.Offset = new Vector(0, 500);
Layout(target);
var newFocused = target.GetRealizedElements().First()!;
newFocused.Focusable = true;
newFocused.Focus();
Assert.False(originalFocused.IsVisible);
@ -635,6 +641,22 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Does_Not_Throw_When_Estimating_Viewport_With_Ancestor_Margin()
{
// Issue #11272
using var app = App();
var (_, _, itemsControl) = CreateUnrootedTarget();
var container = new Decorator { Margin = new Thickness(100) };
var root = new TestRoot(true, container);
root.LayoutManager.ExecuteInitialLayoutPass();
container.Child = itemsControl;
root.LayoutManager.ExecuteLayoutPass();
}
private static IReadOnlyList<int> GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
{
return target.GetRealizedElements()
@ -681,6 +703,18 @@ namespace Avalonia.Controls.UnitTests
IEnumerable<object>? items = null,
Optional<IDataTemplate?> itemTemplate = default,
IEnumerable<Style>? styles = null)
{
var (target, scroll, itemsControl) = CreateUnrootedTarget(items, itemTemplate);
var root = CreateRoot(itemsControl, styles);
root.LayoutManager.ExecuteInitialLayoutPass();
return (target, scroll, itemsControl);
}
private static (VirtualizingStackPanel, ScrollViewer, ItemsControl) CreateUnrootedTarget(
IEnumerable<object>? items = null,
Optional<IDataTemplate?> itemTemplate = default)
{
var target = new VirtualizingStackPanel();
@ -691,8 +725,8 @@ namespace Avalonia.Controls.UnitTests
[~ItemsPresenter.ItemsPanelProperty] = new TemplateBinding(ItemsPresenter.ItemsPanelProperty),
};
var scroll = new ScrollViewer
{
var scroll = new ScrollViewer
{
Name = "PART_ScrollViewer",
Content = presenter,
Template = ScrollViewerTemplate(),
@ -706,15 +740,18 @@ namespace Avalonia.Controls.UnitTests
ItemTemplate = itemTemplate.GetValueOrDefault(DefaultItemTemplate()),
};
var root = new TestRoot(true, itemsControl);
return (target, scroll, itemsControl);
}
private static TestRoot CreateRoot(Control? child, IEnumerable<Style>? styles = null)
{
var root = new TestRoot(true, child);
root.ClientSize = new(100, 100);
if (styles is not null)
root.Styles.AddRange(styles);
root.LayoutManager.ExecuteInitialLayoutPass();
return (target, scroll, itemsControl);
return root;
}
private static IDataTemplate DefaultItemTemplate()

4
tests/Avalonia.LeakTests/ControlTests.cs

@ -558,7 +558,7 @@ namespace Avalonia.LeakTests
control.ContextMenu = null;
}
var window = new Window();
var window = new Window { Focusable = true };
window.Show();
Assert.Same(window, FocusManager.Instance.Current);
@ -605,7 +605,7 @@ namespace Avalonia.LeakTests
contextMenu.Close();
}
var window = new Window();
var window = new Window { Focusable = true };
window.Show();
Assert.Same(window, FocusManager.Instance.Current);

179
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs

@ -2,6 +2,8 @@
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Xunit;
@ -9,87 +11,146 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
{
public class ResourceIncludeTests : XamlTestBase
{
public class StaticResourceExtensionTests : XamlTestBase
[Fact]
public void ResourceInclude_Loads_ResourceDictionary()
{
[Fact]
public void ResourceInclude_Loads_ResourceDictionary()
var documents = new[]
{
var documents = new[]
{
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resource.xaml"), @"
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resource.xaml"), @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
</ResourceDictionary>"),
new RuntimeXamlLoaderDocument(@"
new RuntimeXamlLoaderDocument(@"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source='avares://Tests/Resource.xaml'/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Border Name='border' Background='{StaticResource brush}'/>
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source='avares://Tests/Resource.xaml'/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Border Name='border' Background='{StaticResource brush}'/>
</UserControl>")
};
};
using (StartWithResources())
{
var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
var userControl = Assert.IsType<UserControl>(compiled[1]);
var border = userControl.FindControl<Border>("border");
using (StartWithResources())
{
var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
var userControl = Assert.IsType<UserControl>(compiled[1]);
var border = userControl.FindControl<Border>("border");
var brush = (ISolidColorBrush)border.Background;
Assert.Equal(0xff506070, brush.Color.ToUInt32());
}
var brush = (ISolidColorBrush)border.Background;
Assert.Equal(0xff506070, brush.Color.ToUInt32());
}
}
[Fact]
public void Missing_ResourceKey_In_ResourceInclude_Does_Not_Cause_StackOverflow()
[Fact]
public void Missing_ResourceKey_In_ResourceInclude_Does_Not_Cause_StackOverflow()
{
var app = Application.Current;
var documents = new[]
{
var app = Application.Current;
var documents = new[]
{
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resource.xaml"), @"
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resource.xaml"), @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<StaticResource x:Key='brush' ResourceKey='missing' />
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<StaticResource x:Key='brush' ResourceKey='missing' />
</ResourceDictionary>"),
new RuntimeXamlLoaderDocument(app, @"
new RuntimeXamlLoaderDocument(app, @"
<Application xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source='avares://Tests/Resource.xaml'/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source='avares://Tests/Resource.xaml'/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>")
};
};
using (StartWithResources())
using (StartWithResources())
{
try
{
try
{
AvaloniaRuntimeXamlLoader.LoadGroup(documents);
}
catch (KeyNotFoundException)
{
}
AvaloniaRuntimeXamlLoader.LoadGroup(documents);
}
catch (KeyNotFoundException)
{
}
}
}
[Fact]
public void ResourceInclude_Should_Be_Allowed_To_Have_Key_In_Custom_Container()
{
var app = Application.Current;
var documents = new[]
{
new RuntimeXamlLoaderDocument(new Uri("avares://Demo/en-us.axaml"), @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<x:String x:Key='OkButton'>OK</x:String>
</ResourceDictionary>"),
new RuntimeXamlLoaderDocument(app, @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'>
<ResourceDictionary.MergedDictionaries>
<local:LocaleCollection>
<ResourceInclude Source='avares://Demo/en-us.axaml' x:Key='English' />
</local:LocaleCollection>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>")
};
using (StartWithResources())
{
var groups = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
var res = Assert.IsType<ResourceDictionary>(groups[1]);
Assert.True(res.TryGetResource("OkButton", null, out var val));
Assert.Equal("OK", val);
}
}
private IDisposable StartWithResources(params (string, string)[] assets)
private IDisposable StartWithResources(params (string, string)[] assets)
{
var assetLoader = new MockAssetLoader(assets);
var services = new TestServices(assetLoader: assetLoader);
return UnitTestApplication.Start(services);
}
}
// See https://github.com/AvaloniaUI/Avalonia/issues/11172
public class LocaleCollection : IResourceProvider
{
private readonly Dictionary<object, IResourceProvider> _langs = new();
public IResourceHost Owner { get; private set; }
public bool HasResources => true;
public event EventHandler OwnerChanged;
public void AddOwner(IResourceHost owner) => Owner = owner;
public void RemoveOwner(IResourceHost owner) => Owner = null;
public bool TryGetResource(object key, ThemeVariant theme, out object? value)
{
if (_langs.TryGetValue("English", out var res))
{
var assetLoader = new MockAssetLoader(assets);
var services = new TestServices(assetLoader: assetLoader);
return UnitTestApplication.Start(services);
return res.TryGetResource(key, theme, out value);
}
value = null;
return false;
}
// Allow Avalonia to use this class as a collection, requires x:Key on the IResourceProvider
public void Add(object k, IResourceProvider v) => _langs.Add(k, v);
}
}

17
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs

@ -385,6 +385,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal(Colors.Blue, ((ISolidColorBrush)userControl.FindResource("brush")!).Color);
}
[Fact]
public void ResourceDictionary_Can_Be_Put_Inside_Of_ResourceDictionary()
{
using (StyledWindow())
{
var xaml = @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<ResourceDictionary x:Key='NotAThemeVariantKey' />
</ResourceDictionary>";
var resources = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml);
var nested = (ResourceDictionary)resources["NotAThemeVariantKey"];
Assert.NotNull(nested);
}
}
private IDisposable StyledWindow(params (string, string)[] assets)
{
var services = TestServices.StyledWindow.With(

Loading…
Cancel
Save