Browse Source

Merge branch 'master' into compiled-bindings

pull/3001/head
Jeremy Koritzinsky 7 years ago
committed by GitHub
parent
commit
7cd9db2d60
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .editorconfig
  2. 2
      azure-pipelines.yml
  3. 2
      build/SkiaSharp.props
  4. 37
      nukebuild/Build.cs
  5. 3
      readme.md
  6. 2
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  7. 21
      samples/ControlCatalog.NetCore/Program.cs
  8. 2
      samples/ControlCatalog/MainView.xaml
  9. 4
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
  10. 25
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  11. 71
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  12. 33
      samples/ControlCatalog/Pages/TabStripPage.xaml
  13. 45
      samples/ControlCatalog/Pages/TabStripPage.xaml.cs
  14. 3
      samples/ControlCatalog/SideBar.xaml
  15. 3
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  16. 5
      samples/RenderDemo/SideBar.xaml
  17. 1
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  18. 2
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  19. 2
      src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
  20. 103
      src/Avalonia.Base/AvaloniaObject.cs
  21. 2
      src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
  22. 9
      src/Avalonia.Base/IPriorityValueOwner.cs
  23. 34
      src/Avalonia.Base/PriorityValue.cs
  24. 150
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  25. 206
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  26. 152
      src/Avalonia.Base/ValueStore.cs
  27. 1
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  28. 3
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  29. 26
      src/Avalonia.Controls/AutoCompleteBox.cs
  30. 17
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  31. 3
      src/Avalonia.Controls/ComboBox.cs
  32. 40
      src/Avalonia.Controls/ContextMenu.cs
  33. 12
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  34. 17
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  35. 14
      src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
  36. 5
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  37. 1
      src/Avalonia.Controls/GridSplitter.cs
  38. 9
      src/Avalonia.Controls/IScrollAnchorProvider.cs
  39. 2
      src/Avalonia.Controls/Image.cs
  40. 15
      src/Avalonia.Controls/ItemsControl.cs
  41. 2
      src/Avalonia.Controls/LayoutTransformControl.cs
  42. 8
      src/Avalonia.Controls/ListBox.cs
  43. 47
      src/Avalonia.Controls/Menu.cs
  44. 5
      src/Avalonia.Controls/MenuBase.cs
  45. 2
      src/Avalonia.Controls/Panel.cs
  46. 85
      src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
  47. 2
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  48. 2
      src/Avalonia.Controls/Presenters/ItemContainerSync.cs
  49. 1
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  50. 2
      src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
  51. 9
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  52. 15
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  53. 19
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  54. 35
      src/Avalonia.Controls/Primitives/Popup.cs
  55. 1
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  56. 59
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  57. 10
      src/Avalonia.Controls/Primitives/TabStrip.cs
  58. 1
      src/Avalonia.Controls/Primitives/Track.cs
  59. 7
      src/Avalonia.Controls/ProgressBar.cs
  60. 54
      src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs
  61. 724
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  62. 24
      src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs
  63. 44
      src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs
  64. 35
      src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs
  65. 143
      src/Avalonia.Controls/Repeater/ItemsSourceView.cs
  66. 106
      src/Avalonia.Controls/Repeater/RecyclePool.cs
  67. 65
      src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs
  68. 54
      src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs
  69. 682
      src/Avalonia.Controls/Repeater/ViewManager.cs
  70. 501
      src/Avalonia.Controls/Repeater/ViewportManager.cs
  71. 118
      src/Avalonia.Controls/Repeater/VirtualizationInfo.cs
  72. 15
      src/Avalonia.Controls/ScrollViewer.cs
  73. 1
      src/Avalonia.Controls/Slider.cs
  74. 149
      src/Avalonia.Controls/StackPanel.cs
  75. 35
      src/Avalonia.Controls/Templates/FuncMemberSelector.cs
  76. 18
      src/Avalonia.Controls/Templates/IMemberSelector.cs
  77. 29
      src/Avalonia.Controls/TextBlock.cs
  78. 105
      src/Avalonia.Controls/TreeView.cs
  79. 1
      src/Avalonia.Controls/WrapPanel.cs
  80. 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  81. 27
      src/Avalonia.Diagnostics/DevTools.xaml
  82. 37
      src/Avalonia.Diagnostics/DevTools.xaml.cs
  83. 7
      src/Avalonia.Diagnostics/Models/EventChainLink.cs
  84. 4
      src/Avalonia.Diagnostics/ViewLocator.cs
  85. 12
      src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs
  86. 64
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  87. 15
      src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs
  88. 9
      src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs
  89. 17
      src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs
  90. 13
      src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs
  91. 12
      src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs
  92. 16
      src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs
  93. 3
      src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs
  94. 16
      src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs
  95. 15
      src/Avalonia.Diagnostics/ViewModels/TreeNode.cs
  96. 29
      src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs
  97. 7
      src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs
  98. 5
      src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs
  99. 33
      src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
  100. 100
      src/Avalonia.Diagnostics/Views/EventsView.xaml

3
.editorconfig

@ -131,13 +131,14 @@ csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
space_within_single_line_array_initializer_braces = true
# Wrapping preferences
csharp_wrap_before_ternary_opsigns = false
# Xaml files
[*.xaml]
indent_size = 4
indent_size = 2
# Xml project files
[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]

2
azure-pipelines.yml

@ -102,7 +102,7 @@ jobs:
- job: Windows
pool:
vmImage: 'vs2017-win2016'
vmImage: 'windows-2019'
steps:
- task: CmdLine@2
displayName: 'Install Nuke'

2
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.68.0" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.2" />
</ItemGroup>
</Project>

37
nukebuild/Build.cs

@ -89,6 +89,29 @@ partial class Build : NukeBuild
}
IReadOnlyCollection<Output> MsBuildCommon(
string projectFile,
Configure<MSBuildSettings> configurator = null)
{
return MSBuild(projectFile, c =>
{
// This is required for VS2019 image on Azure Pipelines
if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure)
{
var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64");
if (javaSdk != null)
c = c.AddProperty("JavaSdkDirectory", javaSdk);
}
c = c.AddProperty("PackageVersion", Parameters.Version)
.AddProperty("iOSRoslynPathHackRequired", "true")
.SetToolPath(MsBuildExe.Value)
.SetConfiguration(Parameters.Configuration)
.SetVerbosity(MSBuildVerbosity.Minimal);
c = configurator?.Invoke(c) ?? c;
return c;
});
}
Target Clean => _ => _.Executes(() =>
{
DeleteDirectories(Parameters.BuildDirs);
@ -105,13 +128,8 @@ partial class Build : NukeBuild
.Executes(() =>
{
if (Parameters.IsRunningOnWindows)
MSBuild(Parameters.MSBuildSolution, c => c
MsBuildCommon(Parameters.MSBuildSolution, c => c
.SetArgumentConfigurator(a => a.Add("/r"))
.SetConfiguration(Parameters.Configuration)
.SetVerbosity(MSBuildVerbosity.Minimal)
.AddProperty("PackageVersion", Parameters.Version)
.AddProperty("iOSRoslynPathHackRequired", "true")
.SetToolPath(MsBuildExe.Value)
.AddTargets("Build")
);
@ -237,12 +255,7 @@ partial class Build : NukeBuild
{
if (Parameters.IsRunningOnWindows)
MSBuild(Parameters.MSBuildSolution, c => c
.SetConfiguration(Parameters.Configuration)
.SetVerbosity(MSBuildVerbosity.Minimal)
.AddProperty("PackageVersion", Parameters.Version)
.AddProperty("iOSRoslynPathHackRequired", "true")
.SetToolPath(MsBuildExe.Value)
MsBuildCommon(Parameters.MSBuildSolution, c => c
.AddTargets("Pack"));
else
DotNetPack(Parameters.MSBuildSolution, c =>

3
readme.md

@ -32,9 +32,6 @@ Install-Package Avalonia.Desktop
## Bleeding Edge Builds
Try out the latest build of Avalonia available for download here:
https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master/artifacts
or use nightly build feeds as described here:
https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed

2
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@ -16,7 +16,7 @@
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
<TargetFrameworkVersion>v4.4</TargetFrameworkVersion>
<TargetFrameworkVersion>v8.0</TargetFrameworkVersion>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

21
samples/ControlCatalog.NetCore/Program.cs

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
using Avalonia.LinuxFramebuffer.Output;
using Avalonia.Skia;
using Avalonia.ReactiveUI;
@ -29,8 +30,13 @@ namespace ControlCatalog.NetCore
var builder = BuildAvaloniaApp();
if (args.Contains("--fbdev"))
{
System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer());
return builder.StartLinuxFramebuffer(args);
SilenceConsole();
return builder.StartLinuxFbDev(args);
}
else if (args.Contains("--drm"))
{
SilenceConsole();
return builder.StartLinuxDrm(args);
}
else
return builder.StartWithClassicDesktopLifetime(args);
@ -51,11 +57,14 @@ namespace ControlCatalog.NetCore
.UseSkia()
.UseReactiveUI();
static void ConsoleSilencer()
static void SilenceConsole()
{
Console.CursorVisible = false;
while (true)
Console.ReadKey(true);
new Thread(() =>
{
Console.CursorVisible = false;
while (true)
Console.ReadKey(true);
}) {IsBackground = true}.Start();
}
}
}

2
samples/ControlCatalog/MainView.xaml

@ -26,6 +26,7 @@
<TabItem Header="Drag+Drop"><pages:DragAndDropPage/></TabItem>
<TabItem Header="Expander"><pages:ExpanderPage/></TabItem>
<TabItem Header="Image"><pages:ImagePage/></TabItem>
<TabItem Header="ItemsRepeater"><pages:ItemsRepeaterPage/></TabItem>
<TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem>
<TabItem Header="ListBox"><pages:ListBoxPage/></TabItem>
<TabItem Header="Menu"><pages:MenuPage/></TabItem>
@ -36,6 +37,7 @@
<TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>
<TabItem Header="Slider"><pages:SliderPage/></TabItem>
<TabItem Header="TabControl"><pages:TabControlPage/></TabItem>
<TabItem Header="TabStrip"><pages:TabStripPage/></TabItem>
<TabItem Header="TextBox"><pages:TextBoxPage/></TabItem>
<TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem>
<TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>

4
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml

@ -37,10 +37,6 @@
<StackPanel Orientation="Vertical">
<TextBlock Text="ValueMemeberSelector"/>
<AutoCompleteBox Width="200"
Margin="0,0,0,8"
ValueMemberSelector="Capital"/>
<TextBlock Text="ValueMemberBinding"/>
<AutoCompleteBox Width="200"
Margin="0,0,0,8"

25
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -0,0 +1,25 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ItemsRepeaterPage">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Spacing="4" Margin="0 0 0 16">
<TextBlock Classes="h1">ItemsRepeater</TextBlock>
<TextBlock Classes="h2">A data-driven collection control that incorporates a flexible layout system, custom views, and virtualization.</TextBlock>
</StackPanel>
<StackPanel DockPanel.Dock="Right" Margin="8 0">
<ComboBox SelectedIndex="0" SelectionChanged="LayoutChanged">
<ComboBoxItem>Stack - Vertical</ComboBoxItem>
<ComboBoxItem>Stack - Horizontal</ComboBoxItem>
<ComboBoxItem>UniformGrid - Vertical</ComboBoxItem>
<ComboBoxItem>UniformGrid - Horizontal</ComboBoxItem>
</ComboBox>
</StackPanel>
<Border BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}" Margin="0 0 0 16">
<ScrollViewer Name="scroller"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ItemsRepeater Name="repeater" Items="{Binding}"/>
</ScrollViewer>
</Border>
</DockPanel>
</UserControl>

71
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs

@ -0,0 +1,71 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages
{
public class ItemsRepeaterPage : UserControl
{
private ItemsRepeater _repeater;
private ScrollViewer _scroller;
public ItemsRepeaterPage()
{
this.InitializeComponent();
_repeater = this.FindControl<ItemsRepeater>("repeater");
_scroller = this.FindControl<ScrollViewer>("scroller");
DataContext = Enumerable.Range(1, 100000).Select(i => $"Item {i}" ).ToArray();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void LayoutChanged(object sender, SelectionChangedEventArgs e)
{
if (_repeater == null)
{
return;
}
var comboBox = (ComboBox)sender;
switch (comboBox.SelectedIndex)
{
case 0:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
_repeater.Layout = new StackLayout { Orientation = Orientation.Vertical };
break;
case 1:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
_repeater.Layout = new StackLayout { Orientation = Orientation.Horizontal };
break;
case 2:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
_repeater.Layout = new UniformGridLayout
{
Orientation = Orientation.Vertical,
MinItemWidth = 200,
MinItemHeight = 200,
};
break;
case 3:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
_repeater.Layout = new UniformGridLayout
{
Orientation = Orientation.Horizontal,
MinItemWidth = 200,
MinItemHeight = 200,
};
break;
}
}
}
}

33
samples/ControlCatalog/Pages/TabStripPage.xaml

@ -0,0 +1,33 @@
<UserControl xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.TabStripPage"
xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">TabStrip</TextBlock>
<TextBlock Classes="h2">A control which displays a selectable strip of tabs</TextBlock>
<Separator Margin="0 16"/>
<TextBlock Classes="h1">Defined in XAML</TextBlock>
<TabStrip>
<TabStripItem>Item 1</TabStripItem>
<TabStripItem>Item 2</TabStripItem>
<TabStripItem IsEnabled="False">Disabled</TabStripItem>
</TabStrip>
<Separator Margin="0 16"/>
<TextBlock Classes="h1">Dynamically generated</TextBlock>
<TabStrip Items="{Binding}">
<TabStrip.Styles>
<Style Selector="TabStripItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
</Style>
</TabStrip.Styles>
<TabStrip.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}"/>
</DataTemplate>
</TabStrip.ItemTemplate>
</TabStrip>
</StackPanel>
</UserControl>

45
samples/ControlCatalog/Pages/TabStripPage.xaml.cs

@ -0,0 +1,45 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace ControlCatalog.Pages
{
public class TabStripPage : UserControl
{
public TabStripPage()
{
InitializeComponent();
DataContext = new[]
{
new TabStripItemViewModel
{
Header = "Item 1",
},
new TabStripItemViewModel
{
Header = "Item 2",
},
new TabStripItemViewModel
{
Header = "Disabled",
IsEnabled = false,
},
};
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private class TabStripItemViewModel
{
public string Header { get; set; }
public bool IsEnabled { get; set; } = true;
}
}
}

3
samples/ControlCatalog/SideBar.xaml

@ -29,8 +29,7 @@
Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
MemberSelector="{TemplateBinding MemberSelector}">
ItemTemplate="{TemplateBinding ItemTemplate}">
</ItemsPresenter>
</ScrollViewer>
<ContentPresenter

3
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -1,11 +1,10 @@
using System.Reactive;
using Avalonia.Controls.Notifications;
using Avalonia.Diagnostics.ViewModels;
using ReactiveUI;
namespace ControlCatalog.ViewModels
{
class MainWindowViewModel : ViewModelBase
class MainWindowViewModel : ReactiveObject
{
private IManagedNotificationManager _notificationManager;

5
samples/RenderDemo/SideBar.xaml

@ -20,8 +20,7 @@
Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
MemberSelector="{TemplateBinding MemberSelector}">
ItemTemplate="{TemplateBinding ItemTemplate}">
</ItemsPresenter>
</ScrollViewer>
<ContentPresenter
@ -63,4 +62,4 @@
<Style Selector="TabControl.sidebar > TabItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush2}"/>
</Style>
</Styles>
</Styles>

1
samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs

@ -9,6 +9,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using ReactiveUI.Legacy;
using ReactiveUI;
using Avalonia.Layout;
namespace VirtualizationDemo.ViewModels
{

2
src/Android/Avalonia.Android/Avalonia.Android.csproj

@ -1,6 +1,6 @@
<Project Sdk="MSBuild.Sdk.Extras">
<PropertyGroup>
<TargetFramework>monoandroid44</TargetFramework>
<TargetFramework>monoandroid80</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>

2
src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj

@ -16,7 +16,7 @@
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
<TargetFrameworkVersion>v4.4</TargetFrameworkVersion>
<TargetFrameworkVersion>v8.0</TargetFrameworkVersion>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

103
src/Avalonia.Base/AvaloniaObject.cs

@ -163,6 +163,37 @@ namespace Avalonia
SetValue(property, AvaloniaProperty.UnsetValue);
}
/// <summary>
/// Compares two objects using reference equality.
/// </summary>
/// <param name="obj">The object to compare.</param>
/// <remarks>
/// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons:
///
/// - AvaloniaObjects are by their nature mutable
/// - The presence of attached properties means that the semantics of equality are
/// difficult to define
///
/// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted
/// this.
/// </remarks>
public sealed override bool Equals(object obj) => base.Equals(obj);
/// <summary>
/// Gets the hash code for the object.
/// </summary>
/// <remarks>
/// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons:
///
/// - AvaloniaObjects are by their nature mutable
/// - The presence of attached properties means that the semantics of equality are
/// difficult to define
///
/// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted
/// this.
/// </remarks>
public sealed override int GetHashCode() => base.GetHashCode();
/// <summary>
/// Gets a <see cref="AvaloniaProperty"/> value.
/// </summary>
@ -466,7 +497,7 @@ namespace Avalonia
/// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
protected void RaisePropertyChanged(
protected internal void RaisePropertyChanged(
AvaloniaProperty property,
object oldValue,
object newValue,
@ -508,45 +539,6 @@ namespace Avalonia
}
}
/// <summary>
/// A callback type for encapsulating complex logic for setting direct properties.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="value">The value to which to set the property.</param>
/// <param name="field">The backing field for the property.</param>
/// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
protected delegate void SetAndRaiseCallback<T>(T value, ref T field, Action<Action> notifyWrapper);
/// <summary>
/// Sets the backing field for a direct avalonia property, raising the
/// <see cref="PropertyChanged"/> event if the value has changed.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="field">The backing field.</param>
/// <param name="setterCallback">A callback called to actually set the value to the backing field.</param>
/// <param name="value">The value.</param>
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
protected bool SetAndRaise<T>(
AvaloniaProperty<T> property,
ref T field,
SetAndRaiseCallback<T> setterCallback,
T value)
{
Contract.Requires<ArgumentNullException>(setterCallback != null);
return Values.Setter.SetAndNotify(
property,
ref field,
(object update, ref T backing, Action<Action> notify) =>
{
setterCallback((T)update, ref backing, notify);
return true;
},
value);
}
/// <summary>
/// Sets the backing field for a direct avalonia property, raising the
/// <see cref="PropertyChanged"/> event if the value has changed.
@ -561,32 +553,15 @@ namespace Avalonia
protected bool SetAndRaise<T>(AvaloniaProperty<T> property, ref T field, T value)
{
VerifyAccess();
return SetAndRaise(
property,
ref field,
(T val, ref T backing, Action<Action> notifyWrapper)
=> SetAndRaiseCore(property, ref backing, val, notifyWrapper),
value);
}
/// <summary>
/// Default assignment logic for SetAndRaise.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="field">The backing field.</param>
/// <param name="value">The value.</param>
/// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
private bool SetAndRaiseCore<T>(AvaloniaProperty property, ref T field, T value, Action<Action> notifyWrapper)
{
var old = field;
field = value;
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}
DeferredSetter<T> setter = Values.GetDirectDeferredSetter(property);
notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue));
return true;
return setter.SetAndNotify(this, property, ref field, value);
}
/// <summary>

2
src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs

@ -31,7 +31,7 @@ namespace Avalonia.Data.Converters
{
if (value == null)
{
return AvaloniaProperty.UnsetValue;
return targetType.IsValueType ? AvaloniaProperty.UnsetValue : null;
}
if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1)

9
src/Avalonia.Base/IPriorityValueOwner.cs

@ -29,6 +29,13 @@ namespace Avalonia
/// <param name="notification">The notification.</param>
void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification);
/// <summary>
/// Returns deferred setter for given non-direct property.
/// </summary>
/// <param name="property">Property.</param>
/// <returns>Deferred setter for given property.</returns>
DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property);
/// <summary>
/// Logs a binding error.
/// </summary>
@ -40,7 +47,5 @@ namespace Avalonia
/// Ensures that the current thread is the UI thread.
/// </summary>
void VerifyAccess();
DeferredSetter<object> Setter { get; }
}
}

34
src/Avalonia.Base/PriorityValue.cs

@ -30,7 +30,9 @@ namespace Avalonia
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
private readonly Func<object, object> _validate;
private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback;
private (object value, int priority) _value;
private DeferredSetter<object> _setter;
/// <summary>
/// Initializes a new instance of the <see cref="PriorityValue"/> class.
@ -50,6 +52,7 @@ namespace Avalonia
_valueType = valueType;
_value = (AvaloniaProperty.UnsetValue, int.MaxValue);
_validate = validate;
_setAndNotifyCallback = SetAndNotify;
}
/// <summary>
@ -242,22 +245,22 @@ namespace Avalonia
/// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority)
{
Owner.Setter.SetAndNotify(Property,
ref _value,
UpdateCore,
(value, priority));
}
var newValue = (value, priority);
if (newValue == _value)
{
return;
}
private bool UpdateCore(
object update,
ref (object value, int priority) backing,
Action<Action> notify)
=> UpdateCore(((object, int))update, ref backing, notify);
if (_setter == null)
{
_setter = Owner.GetNonDirectDeferredSetter(Property);
}
_setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue);
}
private bool UpdateCore(
(object value, int priority) update,
ref (object value, int priority) backing,
Action<Action> notify)
private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update)
{
var val = update.value;
var notification = val as BindingNotification;
@ -286,7 +289,7 @@ namespace Avalonia
if (notification == null || notification.HasValue)
{
notify(() => Owner?.Changed(Property, ValuePriority, old, Value));
Owner?.Changed(Property, ValuePriority, old, Value);
}
if (notification != null)
@ -305,7 +308,6 @@ namespace Avalonia
val,
val?.GetType());
}
return true;
}
}
}

150
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@ -0,0 +1,150 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
namespace Avalonia.Utilities
{
/// <summary>
/// Stores values with <see cref="AvaloniaProperty"/> as key.
/// </summary>
/// <typeparam name="TValue">Stored value type.</typeparam>
internal sealed class AvaloniaPropertyValueStore<TValue>
{
private Entry[] _entries;
public AvaloniaPropertyValueStore()
{
// The last item in the list is always int.MaxValue
_entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } };
}
private (int, bool) TryFindEntry(int propertyId)
{
if (_entries.Length <= 12)
{
// For small lists, we use an optimized linear search. Since the last item in the list
// is always int.MaxValue, we can skip a conditional branch in each iteration.
// By unrolling the loop, we can skip another unconditional branch in each iteration.
if (_entries[0].PropertyId >= propertyId)
return (0, _entries[0].PropertyId == propertyId);
if (_entries[1].PropertyId >= propertyId)
return (1, _entries[1].PropertyId == propertyId);
if (_entries[2].PropertyId >= propertyId)
return (2, _entries[2].PropertyId == propertyId);
if (_entries[3].PropertyId >= propertyId)
return (3, _entries[3].PropertyId == propertyId);
if (_entries[4].PropertyId >= propertyId)
return (4, _entries[4].PropertyId == propertyId);
if (_entries[5].PropertyId >= propertyId)
return (5, _entries[5].PropertyId == propertyId);
if (_entries[6].PropertyId >= propertyId)
return (6, _entries[6].PropertyId == propertyId);
if (_entries[7].PropertyId >= propertyId)
return (7, _entries[7].PropertyId == propertyId);
if (_entries[8].PropertyId >= propertyId)
return (8, _entries[8].PropertyId == propertyId);
if (_entries[9].PropertyId >= propertyId)
return (9, _entries[9].PropertyId == propertyId);
if (_entries[10].PropertyId >= propertyId)
return (10, _entries[10].PropertyId == propertyId);
}
else
{
int low = 0;
int high = _entries.Length;
int id;
while (high - low > 3)
{
int pivot = (high + low) / 2;
id = _entries[pivot].PropertyId;
if (propertyId == id)
return (pivot, true);
if (propertyId <= id)
high = pivot;
else
low = pivot + 1;
}
do
{
id = _entries[low].PropertyId;
if (id == propertyId)
return (low, true);
if (id > propertyId)
break;
++low;
}
while (low < high);
}
return (0, false);
}
public bool TryGetValue(AvaloniaProperty property, out TValue value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
{
value = default;
return false;
}
value = _entries[index].Value;
return true;
}
public void AddValue(AvaloniaProperty property, TValue value)
{
Entry[] entries = new Entry[_entries.Length + 1];
for (int i = 0; i < _entries.Length; ++i)
{
if (_entries[i].PropertyId > property.Id)
{
if (i > 0)
{
Array.Copy(_entries, 0, entries, 0, i);
}
entries[i] = new Entry { PropertyId = property.Id, Value = value };
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
break;
}
}
_entries = entries;
}
public void SetValue(AvaloniaProperty property, TValue value)
{
_entries[TryFindEntry(property.Id).Item1].Value = value;
}
public Dictionary<AvaloniaProperty, TValue> ToDictionary()
{
var dict = new Dictionary<AvaloniaProperty, TValue>(_entries.Length - 1);
for (int i = 0; i < _entries.Length - 1; ++i)
{
dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
}
return dict;
}
private struct Entry
{
internal int PropertyId;
internal TValue Value;
}
}
}

206
src/Avalonia.Base/Utilities/DeferredSetter.cs

@ -1,168 +1,122 @@
using System;
using System.Collections.Generic;
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Utilities
{
/// <summary>
/// Callback invoked when deferred setter wants to set a value.
/// </summary>
/// <typeparam name="TValue">Value type.</typeparam>
/// <param name="property">Property being set.</param>
/// <param name="backing">Backing field reference.</param>
/// <param name="value">New value.</param>
internal delegate void SetAndNotifyCallback<TValue>(AvaloniaProperty property, ref TValue backing, TValue value);
/// <summary>
/// A utility class to enable deferring assignment until after property-changed notifications are sent.
/// Used to fix #855.
/// </summary>
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
class DeferredSetter<TSetRecord>
internal sealed class DeferredSetter<TSetRecord>
{
private struct NotifyDisposable : IDisposable
private readonly SingleOrQueue<TSetRecord> _pendingValues;
private bool _isNotifying;
public DeferredSetter()
{
private readonly SettingStatus status;
_pendingValues = new SingleOrQueue<TSetRecord>();
}
internal NotifyDisposable(SettingStatus status)
{
this.status = status;
status.Notifying = true;
}
private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty<TSetRecord> property, ref TSetRecord backing, TSetRecord value)
{
var old = backing;
public void Dispose()
{
status.Notifying = false;
}
backing = value;
source.RaisePropertyChanged(property, old, value);
}
/// <summary>
/// Information on current setting/notification status of a property.
/// </summary>
private class SettingStatus
public bool SetAndNotify(
AvaloniaObject source,
AvaloniaProperty<TSetRecord> property,
ref TSetRecord backing,
TSetRecord value)
{
public bool Notifying { get; set; }
private SingleOrQueue<TSetRecord> pendingValues;
public SingleOrQueue<TSetRecord> PendingValues
if (!_isNotifying)
{
get
using (new NotifyDisposable(this))
{
return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>());
SetAndRaisePropertyChanged(source, property, ref backing, value);
}
}
}
private Dictionary<AvaloniaProperty, SettingStatus> _setRecords;
private Dictionary<AvaloniaProperty, SettingStatus> SetRecords
=> _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>());
if (!_pendingValues.Empty)
{
using (new NotifyDisposable(this))
{
while (!_pendingValues.Empty)
{
SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue());
}
}
}
private SettingStatus GetOrCreateStatus(AvaloniaProperty property)
{
if (!SetRecords.TryGetValue(property, out var status))
{
status = new SettingStatus();
SetRecords.Add(property, status);
return true;
}
return status;
_pendingValues.Enqueue(value);
return false;
}
/// <summary>
/// Mark the property as currently notifying.
/// </summary>
/// <param name="property">The property to mark as notifying.</param>
/// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
private NotifyDisposable MarkNotifying(AvaloniaProperty property)
public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, SetAndNotifyCallback<TValue> setAndNotifyCallback, ref TValue backing, TValue value)
where TValue : TSetRecord
{
Contract.Requires<InvalidOperationException>(!IsNotifying(property));
SettingStatus status = GetOrCreateStatus(property);
return new NotifyDisposable(status);
}
if (!_isNotifying)
{
using (new NotifyDisposable(this))
{
setAndNotifyCallback(property, ref backing, value);
}
/// <summary>
/// Check if the property is currently notifying listeners.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>If the property is currently notifying listeners.</returns>
private bool IsNotifying(AvaloniaProperty property)
=> SetRecords.TryGetValue(property, out var value) && value.Notifying;
if (!_pendingValues.Empty)
{
using (new NotifyDisposable(this))
{
while (!_pendingValues.Empty)
{
setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue());
}
}
}
/// <summary>
/// Add a pending assignment for the property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value to assign.</param>
private void AddPendingSet(AvaloniaProperty property, TSetRecord value)
{
Contract.Requires<InvalidOperationException>(IsNotifying(property));
return true;
}
GetOrCreateStatus(property).PendingValues.Enqueue(value);
}
_pendingValues.Enqueue(value);
/// <summary>
/// Checks if there are any pending assignments for the property.
/// </summary>
/// <param name="property">The property to check.</param>
/// <returns>If the property has any pending assignments.</returns>
private bool HasPendingSet(AvaloniaProperty property)
{
return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty;
return false;
}
/// <summary>
/// Gets the first pending assignment for the property.
/// Disposable that marks the property as currently notifying.
/// When disposed, marks the property as done notifying.
/// </summary>
/// <param name="property">The property to check.</param>
/// <returns>The first pending assignment for the property.</returns>
private TSetRecord GetFirstPendingSet(AvaloniaProperty property)
private readonly struct NotifyDisposable : IDisposable
{
return GetOrCreateStatus(property).PendingValues.Dequeue();
}
public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback);
private readonly DeferredSetter<TSetRecord> _setter;
/// <summary>
/// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824
/// </summary>
/// <param name="property">The property to set.</param>
/// <param name="backing">The backing field for the property</param>
/// <param name="setterCallback">
/// A callback that actually sets the property.
/// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification.
/// </param>
/// <param name="value">The value to try to set.</param>
public bool SetAndNotify<TValue>(
AvaloniaProperty property,
ref TValue backing,
SetterDelegate<TValue> setterCallback,
TSetRecord value)
{
Contract.Requires<ArgumentNullException>(setterCallback != null);
if (!IsNotifying(property))
internal NotifyDisposable(DeferredSetter<TSetRecord> setter)
{
bool updated = false;
if (!object.Equals(value, backing))
{
updated = setterCallback(value, ref backing, notification =>
{
using (MarkNotifying(property))
{
notification();
}
});
}
while (HasPendingSet(property))
{
updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification =>
{
using (MarkNotifying(property))
{
notification();
}
});
}
return updated;
_setter = setter;
_setter._isNotifying = true;
}
else if(!object.Equals(value, backing))
public void Dispose()
{
AddPendingSet(property, value);
_setter._isNotifying = false;
}
return false;
}
}
}

152
src/Avalonia.Base/ValueStore.cs

@ -7,21 +7,15 @@ namespace Avalonia
{
internal class ValueStore : IPriorityValueOwner
{
private struct Entry
{
internal int PropertyId;
internal object Value;
}
private readonly AvaloniaPropertyValueStore<object> _propertyValues;
private readonly AvaloniaPropertyValueStore<object> _deferredSetters;
private readonly AvaloniaObject _owner;
private Entry[] _entries;
public ValueStore(AvaloniaObject owner)
{
_owner = owner;
// The last item in the list is always int.MaxValue
_entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } };
_propertyValues = new AvaloniaPropertyValueStore<object>();
_deferredSetters = new AvaloniaPropertyValueStore<object>();
}
public IDisposable AddBinding(
@ -31,7 +25,7 @@ namespace Avalonia
{
PriorityValue priorityValue;
if (TryGetValue(property, out var v))
if (_propertyValues.TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
@ -39,13 +33,13 @@ namespace Avalonia
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
SetValueInternal(property, priorityValue);
_propertyValues.SetValue(property, priorityValue);
}
}
else
{
priorityValue = CreatePriorityValue(property);
AddValueInternal(property, priorityValue);
_propertyValues.AddValue(property, priorityValue);
}
return priorityValue.Add(source, (int)priority);
@ -55,7 +49,7 @@ namespace Avalonia
{
PriorityValue priorityValue;
if (TryGetValue(property, out var v))
if (_propertyValues.TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
@ -63,7 +57,7 @@ namespace Avalonia
{
if (priority == (int)BindingPriority.LocalValue)
{
SetValueInternal(property, Validate(property, value));
_propertyValues.SetValue(property, Validate(property, value));
Changed(property, priority, v, value);
return;
}
@ -71,7 +65,7 @@ namespace Avalonia
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
SetValueInternal(property, priorityValue);
_propertyValues.SetValue(property, priorityValue);
}
}
}
@ -84,14 +78,14 @@ namespace Avalonia
if (priority == (int)BindingPriority.LocalValue)
{
AddValueInternal(property, Validate(property, value));
_propertyValues.AddValue(property, Validate(property, value));
Changed(property, priority, AvaloniaProperty.UnsetValue, value);
return;
}
else
{
priorityValue = CreatePriorityValue(property);
AddValueInternal(property, priorityValue);
_propertyValues.AddValue(property, priorityValue);
}
}
@ -110,14 +104,9 @@ namespace Avalonia
public IDictionary<AvaloniaProperty, object> GetSetValues()
{
var dict = new Dictionary<AvaloniaProperty, object>(_entries.Length - 1);
for (int i = 0; i < _entries.Length - 1; ++i)
{
dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
}
return dict;
return _propertyValues.ToDictionary();
}
public void LogError(AvaloniaProperty property, Exception e)
{
_owner.LogBindingError(property, e);
@ -127,7 +116,7 @@ namespace Avalonia
{
var result = AvaloniaProperty.UnsetValue;
if (TryGetValue(property, out var value))
if (_propertyValues.TryGetValue(property, out var value))
{
result = (value is PriorityValue priorityValue) ? priorityValue.Value : value;
}
@ -137,12 +126,12 @@ namespace Avalonia
public bool IsAnimating(AvaloniaProperty property)
{
return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
}
public bool IsSet(AvaloniaProperty property)
{
if (TryGetValue(property, out var value))
if (_propertyValues.TryGetValue(property, out var value))
{
return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue;
}
@ -152,7 +141,7 @@ namespace Avalonia
public void Revalidate(AvaloniaProperty property)
{
if (TryGetValue(property, out var value))
if (_propertyValues.TryGetValue(property, out var value))
{
(value as PriorityValue)?.Revalidate();
}
@ -189,113 +178,28 @@ namespace Avalonia
return value;
}
private DeferredSetter<object> _deferredSetter;
public DeferredSetter<object> Setter
private DeferredSetter<T> GetDeferredSetter<T>(AvaloniaProperty property)
{
get
if (_deferredSetters.TryGetValue(property, out var deferredSetter))
{
return _deferredSetter ??
(_deferredSetter = new DeferredSetter<object>());
return (DeferredSetter<T>)deferredSetter;
}
}
private bool TryGetValue(AvaloniaProperty property, out object value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
{
value = null;
return false;
}
value = _entries[index].Value;
return true;
}
private void AddValueInternal(AvaloniaProperty property, object value)
{
Entry[] entries = new Entry[_entries.Length + 1];
for (int i = 0; i < _entries.Length; ++i)
{
if (_entries[i].PropertyId > property.Id)
{
if (i > 0)
{
Array.Copy(_entries, 0, entries, 0, i);
}
var newDeferredSetter = new DeferredSetter<T>();
entries[i] = new Entry { PropertyId = property.Id, Value = value };
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
break;
}
}
_deferredSetters.AddValue(property, newDeferredSetter);
_entries = entries;
return newDeferredSetter;
}
private void SetValueInternal(AvaloniaProperty property, object value)
public DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property)
{
_entries[TryFindEntry(property.Id).Item1].Value = value;
return GetDeferredSetter<object>(property);
}
private (int, bool) TryFindEntry(int propertyId)
public DeferredSetter<T> GetDirectDeferredSetter<T>(AvaloniaProperty<T> property)
{
if (_entries.Length <= 12)
{
// For small lists, we use an optimized linear search. Since the last item in the list
// is always int.MaxValue, we can skip a conditional branch in each iteration.
// By unrolling the loop, we can skip another unconditional branch in each iteration.
if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId);
if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId);
if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId);
if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId);
if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId);
if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId);
if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId);
if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId);
if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId);
if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId);
if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId);
}
else
{
int low = 0;
int high = _entries.Length;
int id;
while (high - low > 3)
{
int pivot = (high + low) / 2;
id = _entries[pivot].PropertyId;
if (propertyId == id)
return (pivot, true);
if (propertyId <= id)
high = pivot;
else
low = pivot + 1;
}
do
{
id = _entries[low].PropertyId;
if (id == propertyId)
return (low, true);
if (id > propertyId)
break;
++low;
}
while (low < high);
}
return (0, false);
return GetDeferredSetter<T>(property);
}
}
}

1
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -24,6 +24,7 @@ using System.Linq;
using Avalonia.Input.Platform;
using System.ComponentModel.DataAnnotations;
using Avalonia.Controls.Utils;
using Avalonia.Layout;
namespace Avalonia.Controls
{

3
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@ -195,7 +195,6 @@
<Setter Property="GridLinesVisibility" Value="Vertical" />
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource ThemeBorderLightBrush}" />
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource ThemeBorderLightBrush}" />
<Setter Property="HeadersVisibility" Value="Column" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderDarkBrush}"/>
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}" />
<Setter Property="DropLocationIndicatorTemplate">
@ -230,4 +229,4 @@
</ControlTemplate>
</Setter>
</Style>
</Styles>
</Styles>

26
src/Avalonia.Controls/AutoCompleteBox.cs

@ -345,7 +345,6 @@ namespace Avalonia.Controls
/// </summary>
private IDisposable _collectionChangeSubscription;
private IMemberSelector _valueMemberSelector;
private Func<string, CancellationToken, Task<IEnumerable<object>>> _asyncPopulator;
private CancellationTokenSource _populationCancellationTokenSource;
@ -541,12 +540,6 @@ namespace Avalonia.Controls
o => o.Items,
(o, v) => o.Items = v);
public static readonly DirectProperty<AutoCompleteBox, IMemberSelector> ValueMemberSelectorProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, IMemberSelector>(
nameof(ValueMemberSelector),
o => o.ValueMemberSelector,
(o, v) => o.ValueMemberSelector = v);
public static readonly DirectProperty<AutoCompleteBox, Func<string, CancellationToken, Task<IEnumerable<object>>>> AsyncPopulatorProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, Func<string, CancellationToken, Task<IEnumerable<object>>>>(
nameof(AsyncPopulator),
@ -958,20 +951,6 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Gets or sets the MemberSelector that is used to get values for
/// display in the text portion of the
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
/// </summary>
/// <value>The MemberSelector that is used to get values for display in
/// the text portion of the
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.</value>
public IMemberSelector ValueMemberSelector
{
get { return _valueMemberSelector; }
set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); }
}
/// <summary>
/// Gets or sets the selected item in the drop-down.
/// </summary>
@ -1841,11 +1820,6 @@ namespace Avalonia.Controls
return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty;
}
if (_valueMemberSelector != null)
{
value = _valueMemberSelector.Select(value);
}
return value == null ? String.Empty : value.ToString();
}

17
src/Avalonia.Controls/Calendar/CalendarItem.cs

@ -4,6 +4,7 @@
// All other rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using Avalonia.Data;
@ -193,6 +194,9 @@ namespace Avalonia.Controls.Primitives
{
if (MonthView != null)
{
var childCount = Calendar.RowsPerMonth + Calendar.RowsPerMonth * Calendar.ColumnsPerMonth;
var children = new List<IControl>(childCount);
for (int i = 0; i < Calendar.RowsPerMonth; i++)
{
if (_dayTitleTemplate != null)
@ -201,7 +205,7 @@ namespace Avalonia.Controls.Primitives
cell.DataContext = string.Empty;
cell.SetValue(Grid.RowProperty, 0);
cell.SetValue(Grid.ColumnProperty, i);
MonthView.Children.Add(cell);
children.Add(cell);
}
}
@ -222,13 +226,18 @@ namespace Avalonia.Controls.Primitives
cell.PointerEnter += Cell_MouseEnter;
cell.PointerLeave += Cell_MouseLeave;
cell.Click += Cell_Click;
MonthView.Children.Add(cell);
children.Add(cell);
}
}
MonthView.Children.AddRange(children);
}
if (YearView != null)
{
var childCount = Calendar.RowsPerYear * Calendar.ColumnsPerYear;
var children = new List<IControl>(childCount);
CalendarButton month;
for (int i = 0; i < Calendar.RowsPerYear; i++)
{
@ -246,9 +255,11 @@ namespace Avalonia.Controls.Primitives
month.CalendarLeftMouseButtonUp += Month_CalendarButtonMouseUp;
month.PointerEnter += Month_MouseEnter;
month.PointerLeave += Month_MouseLeave;
YearView.Children.Add(month);
children.Add(month);
}
}
YearView.Children.AddRange(children);
}
}

3
src/Avalonia.Controls/ComboBox.cs

@ -333,8 +333,7 @@ namespace Avalonia.Controls
}
else
{
var selector = MemberSelector;
SelectionBoxItem = selector != null ? selector.Select(item) : item;
SelectionBoxItem = item;
}
}

40
src/Avalonia.Controls/ContextMenu.cs

@ -1,12 +1,13 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
namespace Avalonia.Controls
@ -90,9 +91,14 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param>
public void Open(Control control)
{
if (IsOpen)
{
return;
}
if (_popup == null)
{
_popup = new Popup()
_popup = new Popup
{
PlacementMode = PlacementMode.Pointer,
PlacementTarget = control,
@ -107,7 +113,14 @@ namespace Avalonia.Controls
((ISetLogicalParent)_popup).SetParent(control);
_popup.Child = this;
_popup.IsOpen = true;
IsOpen = true;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuOpenedEvent,
Source = this,
});
}
/// <summary>
@ -115,13 +128,15 @@ namespace Avalonia.Controls
/// </summary>
public override void Close()
{
if (!IsOpen)
{
return;
}
if (_popup != null && _popup.IsVisible)
{
_popup.IsOpen = false;
}
SelectedIndex = -1;
IsOpen = false;
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
@ -129,6 +144,18 @@ namespace Avalonia.Controls
return new MenuItemContainerGenerator(this);
}
private void CloseCore()
{
SelectedIndex = -1;
IsOpen = false;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuClosedEvent,
Source = this,
});
}
private void PopupOpened(object sender, EventArgs e)
{
Focus();
@ -145,8 +172,7 @@ namespace Avalonia.Controls
i.IsSubMenuOpen = false;
}
contextMenu.IsOpen = false;
contextMenu.SelectedIndex = -1;
contextMenu.CloseCore();
}
}

12
src/Avalonia.Controls/Generators/IItemContainerGenerator.cs

@ -49,12 +49,8 @@ namespace Avalonia.Controls.Generators
/// The index of the item of data in the control's items.
/// </param>
/// <param name="item">The item.</param>
/// <param name="selector">An optional member selector.</param>
/// <returns>The created controls.</returns>
ItemContainerInfo Materialize(
int index,
object item,
IMemberSelector selector);
ItemContainerInfo Materialize(int index, object item);
/// <summary>
/// Removes a set of created containers.
@ -84,11 +80,7 @@ namespace Avalonia.Controls.Generators
/// <returns>The removed containers.</returns>
IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count);
bool TryRecycle(
int oldIndex,
int newIndex,
object item,
IMemberSelector selector);
bool TryRecycle(int oldIndex, int newIndex, object item);
/// <summary>
/// Clears all created containers and returns the removed controls.

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

@ -54,13 +54,9 @@ namespace Avalonia.Controls.Generators
public virtual Type ContainerType => null;
/// <inheritdoc/>
public ItemContainerInfo Materialize(
int index,
object item,
IMemberSelector selector)
public ItemContainerInfo Materialize(int index, object item)
{
var i = selector != null ? selector.Select(item) : item;
var container = new ItemContainerInfo(CreateContainer(i), item, index);
var container = new ItemContainerInfo(CreateContainer(item), item, index);
_containers.Add(container.Index, container);
Materialized?.Invoke(this, new ItemContainerEventArgs(container));
@ -138,14 +134,7 @@ namespace Avalonia.Controls.Generators
}
/// <inheritdoc/>
public virtual bool TryRecycle(
int oldIndex,
int newIndex,
object item,
IMemberSelector selector)
{
return false;
}
public virtual bool TryRecycle(int oldIndex, int newIndex, object item) => false;
/// <inheritdoc/>
public virtual IEnumerable<ItemContainerInfo> Clear()

14
src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs

@ -79,11 +79,7 @@ namespace Avalonia.Controls.Generators
}
/// <inheritdoc/>
public override bool TryRecycle(
int oldIndex,
int newIndex,
object item,
IMemberSelector selector)
public override bool TryRecycle(int oldIndex, int newIndex, object item)
{
var container = ContainerFromIndex(oldIndex);
@ -92,16 +88,14 @@ namespace Avalonia.Controls.Generators
throw new IndexOutOfRangeException("Could not recycle container: not materialized.");
}
var i = selector != null ? selector.Select(item) : item;
container.SetValue(ContentProperty, i);
container.SetValue(ContentProperty, item);
if (!(item is IControl))
{
container.DataContext = i;
container.DataContext = item;
}
var info = MoveContainer(oldIndex, newIndex, i);
var info = MoveContainer(oldIndex, newIndex, item);
RaiseRecycled(new ItemContainerEventArgs(info));
return true;

5
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@ -117,10 +117,7 @@ namespace Avalonia.Controls.Generators
return base.RemoveRange(startingIndex, count);
}
public override bool TryRecycle(int oldIndex, int newIndex, object item, IMemberSelector selector)
{
return false;
}
public override bool TryRecycle(int oldIndex, int newIndex, object item) => false;
class WrapperTreeDataTemplate : ITreeDataTemplate
{

1
src/Avalonia.Controls/GridSplitter.cs

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls

9
src/Avalonia.Controls/IScrollAnchorProvider.cs

@ -0,0 +1,9 @@
namespace Avalonia.Controls
{
public interface IScrollAnchorProvider
{
IControl CurrentAnchor { get; }
void RegisterAnchorCandidate(IControl element);
void UnregisterAnchorCandidate(IControl element);
}
}

2
src/Avalonia.Controls/Image.cs

@ -96,7 +96,7 @@ namespace Avalonia.Controls
}
}
return result.Constrain(availableSize);
return result;
}
/// <inheritdoc/>

15
src/Avalonia.Controls/ItemsControl.cs

@ -54,12 +54,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
AvaloniaProperty.Register<ItemsControl, IDataTemplate>(nameof(ItemTemplate));
/// <summary>
/// Defines the <see cref="MemberSelector"/> property.
/// </summary>
public static readonly StyledProperty<IMemberSelector> MemberSelectorProperty =
AvaloniaProperty.Register<ItemsControl, IMemberSelector>(nameof(MemberSelector));
private IEnumerable _items = new AvaloniaList<object>();
private int _itemCount;
private IItemContainerGenerator _itemContainerGenerator;
@ -144,15 +138,6 @@ namespace Avalonia.Controls
set { SetValue(ItemTemplateProperty, value); }
}
/// <summary>
/// Selects a member from <see cref="Items"/> to use as the list item.
/// </summary>
public IMemberSelector MemberSelector
{
get { return GetValue(MemberSelectorProperty); }
set { SetValue(MemberSelectorProperty, value); }
}
/// <summary>
/// Gets the items presenter control.
/// </summary>

2
src/Avalonia.Controls/LayoutTransformControl.cs

@ -45,7 +45,7 @@ namespace Avalonia.Controls
}
/// <summary>
/// Utilize the <see cref="RenderTransformProperty"/> for layout transforms.
/// Utilize the <see cref="Visual.RenderTransformProperty"/> for layout transforms.
/// </summary>
public bool UseRenderTransform
{

8
src/Avalonia.Controls/ListBox.cs

@ -68,7 +68,13 @@ namespace Avalonia.Controls
/// <inheritdoc/>
public new IList SelectedItems => base.SelectedItems;
/// <inheritdoc/>
/// <summary>
/// Gets or sets the selection mode.
/// </summary>
/// <remarks>
/// Note that the selection mode only applies to selections made via user interaction.
/// Multiple selections can be made programatically regardless of the value of this property.
/// </remarks>
public new SelectionMode SelectionMode
{
get { return base.SelectionMode; }

47
src/Avalonia.Controls/Menu.cs

@ -5,6 +5,7 @@ using Avalonia.Controls.Platform;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace Avalonia.Controls
{
@ -40,37 +41,41 @@ namespace Avalonia.Controls
/// <inheritdoc/>
public override void Close()
{
if (IsOpen)
if (!IsOpen)
{
foreach (var i in ((IMenu)this).SubItems)
{
i.Close();
}
IsOpen = false;
SelectedIndex = -1;
return;
}
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuClosedEvent,
Source = this,
});
foreach (var i in ((IMenu)this).SubItems)
{
i.Close();
}
IsOpen = false;
SelectedIndex = -1;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuClosedEvent,
Source = this,
});
}
/// <inheritdoc/>
public override void Open()
{
if (!IsOpen)
if (IsOpen)
{
IsOpen = true;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuOpenedEvent,
Source = this,
});
return;
}
IsOpen = true;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuOpenedEvent,
Source = this,
});
}
/// <inheritdoc/>

5
src/Avalonia.Controls/MenuBase.cs

@ -7,7 +7,6 @@ using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
@ -31,13 +30,13 @@ namespace Avalonia.Controls
/// Defines the <see cref="MenuOpened"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> MenuOpenedEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuOpened), RoutingStrategies.Bubble);
RoutedEvent.Register<MenuBase, RoutedEventArgs>(nameof(MenuOpened), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="MenuClosed"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> MenuClosedEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuClosed), RoutingStrategies.Bubble);
RoutedEvent.Register<MenuBase, RoutedEventArgs>(nameof(MenuClosed), RoutingStrategies.Bubble);
private bool _isOpen;

2
src/Avalonia.Controls/Panel.cs

@ -112,7 +112,7 @@ namespace Avalonia.Controls
case NotifyCollectionChangedAction.Add:
controls = e.NewItems.OfType<Control>().ToList();
LogicalChildren.InsertRange(e.NewStartingIndex, controls);
VisualChildren.AddRange(e.NewItems.OfType<Visual>());
VisualChildren.InsertRange(e.NewStartingIndex, e.NewItems.OfType<Visual>());
break;
case NotifyCollectionChangedAction.Move:

85
src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs

@ -9,94 +9,69 @@ using Avalonia.Threading;
namespace Avalonia.Controls.Platform
{
public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderTimer
public class InternalPlatformThreadingInterface : IPlatformThreadingInterface
{
public InternalPlatformThreadingInterface()
{
TlsCurrentThreadIsLoopThread = true;
StartTimer(
DispatcherPriority.Render,
new TimeSpan(0, 0, 0, 0, 66),
() => Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount)));
}
private readonly AutoResetEvent _signaled = new AutoResetEvent(false);
private readonly AutoResetEvent _queued = new AutoResetEvent(false);
private readonly Queue<Action> _actions = new Queue<Action>();
public void RunLoop(CancellationToken cancellationToken)
{
var handles = new[] {_signaled, _queued};
while (true)
{
if (0 == WaitHandle.WaitAny(handles))
Signaled?.Invoke(null);
else
{
while (true)
{
Action item;
lock (_actions)
if (_actions.Count == 0)
break;
else
item = _actions.Dequeue();
item();
}
}
Signaled?.Invoke(null);
_signaled.WaitOne();
}
}
public void Send(Action cb)
{
lock (_actions)
{
_actions.Enqueue(cb);
_queued.Set();
}
}
class WatTimer : IDisposable
class TimerImpl : IDisposable
{
private readonly IDisposable _timer;
private readonly DispatcherPriority _priority;
private readonly TimeSpan _interval;
private readonly Action _tick;
private Timer _timer;
private GCHandle _handle;
public WatTimer(IDisposable timer)
public TimerImpl(DispatcherPriority priority, TimeSpan interval, Action tick)
{
_timer = timer;
_priority = priority;
_interval = interval;
_tick = tick;
_timer = new Timer(OnTimer, null, interval, TimeSpan.FromMilliseconds(-1));
_handle = GCHandle.Alloc(_timer);
}
private void OnTimer(object state)
{
if (_timer == null)
return;
Dispatcher.UIThread.Post(() =>
{
if (_timer == null)
return;
_tick();
_timer?.Change(_interval, TimeSpan.FromMilliseconds(-1));
});
}
public void Dispose()
{
_handle.Free();
_timer.Dispose();
_timer = null;
}
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
return new WatTimer(new System.Threading.Timer(delegate
{
var tcs = new TaskCompletionSource<int>();
Send(() =>
{
try
{
tick();
}
finally
{
tcs.SetResult(0);
}
});
tcs.Task.Wait();
}, null, TimeSpan.Zero, interval));
return new TimerImpl(priority, interval, tick);
}
public void Signal(DispatcherPriority prio)

2
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@ -213,7 +213,7 @@ namespace Avalonia.Controls.Presenters
if (container == null && IsVirtualized)
{
var item = Items.Cast<object>().ElementAt(index);
var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector);
var materialized = ItemContainerGenerator.Materialize(index, item);
Panel.Children.Add(materialized.ContainerControl);
container = materialized.ContainerControl;
}

2
src/Avalonia.Controls/Presenters/ItemContainerSync.cs

@ -88,7 +88,7 @@ namespace Avalonia.Controls.Presenters
foreach (var item in items)
{
var i = generator.Materialize(index++, item, owner.MemberSelector);
var i = generator.Materialize(index++, item);
if (i.ContainerControl != null)
{

1
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@ -8,6 +8,7 @@ using System.Reactive.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Layout;
namespace Avalonia.Controls.Presenters
{

2
src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs

@ -90,7 +90,7 @@ namespace Avalonia.Controls.Presenters
foreach (var item in items)
{
var i = generator.Materialize(index++, item, Owner.MemberSelector);
var i = generator.Materialize(index++, item);
if (i.ContainerControl != null)
{

9
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -314,7 +314,6 @@ namespace Avalonia.Controls.Presenters
if (!panel.IsFull && Items != null && panel.IsAttachedToVisualTree)
{
var memberSelector = Owner.MemberSelector;
var index = NextIndex;
var step = 1;
@ -337,7 +336,7 @@ namespace Avalonia.Controls.Presenters
}
}
var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector);
var materialized = generator.Materialize(index, Items.ElementAt(index));
if (step == 1)
{
@ -383,7 +382,6 @@ namespace Avalonia.Controls.Presenters
{
var panel = VirtualizingPanel;
var generator = Owner.ItemContainerGenerator;
var selector = Owner.MemberSelector;
var containers = generator.Containers.ToList();
var itemIndex = FirstIndex;
@ -393,7 +391,7 @@ namespace Avalonia.Controls.Presenters
if (!object.Equals(container.Item, item))
{
if (!generator.TryRecycle(itemIndex, itemIndex, item, selector))
if (!generator.TryRecycle(itemIndex, itemIndex, item))
{
throw new NotImplementedException();
}
@ -420,7 +418,6 @@ namespace Avalonia.Controls.Presenters
{
var panel = VirtualizingPanel;
var generator = Owner.ItemContainerGenerator;
var selector = Owner.MemberSelector;
//validate delta it should never overflow last index or generate index < 0
delta = MathUtilities.Clamp(delta, -FirstIndex, ItemCount - FirstIndex - panel.Children.Count);
@ -437,7 +434,7 @@ namespace Avalonia.Controls.Presenters
var item = Items.ElementAt(newItemIndex);
if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector))
if (!generator.TryRecycle(oldItemIndex, newItemIndex, item))
{
throw new NotImplementedException();
}

15
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@ -35,12 +35,6 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
ItemsControl.ItemTemplateProperty.AddOwner<ItemsPresenterBase>();
/// <summary>
/// Defines the <see cref="MemberSelector"/> property.
/// </summary>
public static readonly StyledProperty<IMemberSelector> MemberSelectorProperty =
ItemsControl.MemberSelectorProperty.AddOwner<ItemsPresenterBase>();
private IEnumerable _items;
private IDisposable _itemsSubscription;
private bool _createdPanel;
@ -127,15 +121,6 @@ namespace Avalonia.Controls.Presenters
set { SetValue(ItemTemplateProperty, value); }
}
/// <summary>
/// Selects a member from <see cref="Items"/> to use as the list item.
/// </summary>
public IMemberSelector MemberSelector
{
get { return GetValue(MemberSelectorProperty); }
set { SetValue(MemberSelectorProperty, value); }
}
/// <summary>
/// Gets the panel used to display the items.
/// </summary>

19
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -49,6 +49,14 @@ namespace Avalonia.Controls.Presenters
AffectsRender<TextPresenter>(PasswordCharProperty,
SelectionBrushProperty, SelectionForegroundBrushProperty,
SelectionStartProperty, SelectionEndProperty);
Observable.Merge(
SelectionStartProperty.Changed,
SelectionEndProperty.Changed,
PasswordCharProperty.Changed
).AddClassHandler<TextPresenter>((x,_) => x.InvalidateFormattedText());
CaretIndexProperty.Changed.AddClassHandler<TextPresenter>((x, e) => x.CaretIndexChanged((int)e.NewValue));
}
public TextPresenter()
@ -56,17 +64,6 @@ namespace Avalonia.Controls.Presenters
_caretTimer = new DispatcherTimer();
_caretTimer.Interval = TimeSpan.FromMilliseconds(500);
_caretTimer.Tick += CaretTimerTick;
Observable.Merge(
this.GetObservable(SelectionStartProperty),
this.GetObservable(SelectionEndProperty))
.Subscribe(_ => InvalidateFormattedText());
this.GetObservable(CaretIndexProperty)
.Subscribe(CaretIndexChanged);
this.GetObservable(PasswordCharProperty)
.Subscribe(_ => InvalidateFormattedText());
}
public int CaretIndex

35
src/Avalonia.Controls/Primitives/Popup.cs

@ -6,7 +6,6 @@ using System.Linq;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.VisualTree;
@ -270,9 +269,10 @@ namespace Avalonia.Controls.Primitives
_popupRoot.SnapInsideScreenEdges();
}
_ignoreIsOpenChanged = true;
IsOpen = true;
_ignoreIsOpenChanged = false;
using (BeginIgnoringIsOpen())
{
IsOpen = true;
}
Opened?.Invoke(this, EventArgs.Empty);
}
@ -305,7 +305,11 @@ namespace Avalonia.Controls.Primitives
_popupRoot.Hide();
}
IsOpen = false;
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
Closed?.Invoke(this, EventArgs.Empty);
}
@ -467,5 +471,26 @@ namespace Avalonia.Controls.Primitives
Close();
}
}
private IgnoreIsOpenScope BeginIgnoringIsOpen()
{
return new IgnoreIsOpenScope(this);
}
private readonly struct IgnoreIsOpenScope : IDisposable
{
private readonly Popup _owner;
public IgnoreIsOpenScope(Popup owner)
{
_owner = owner;
_owner._ignoreIsOpenChanged = true;
}
public void Dispose()
{
_owner._ignoreIsOpenChanged = false;
}
}
}
}

1
src/Avalonia.Controls/Primitives/ScrollBar.cs

@ -7,6 +7,7 @@ using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Input;
using Avalonia.Layout;
namespace Avalonia.Controls.Primitives
{

59
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -222,6 +222,10 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Gets or sets the selection mode.
/// </summary>
/// <remarks>
/// Note that the selection mode only applies to selections made via user interaction.
/// Multiple selections can be made programatically regardless of the value of this property.
/// </remarks>
protected SelectionMode SelectionMode
{
get { return GetValue(SelectionModeProperty); }
@ -329,6 +333,11 @@ namespace Avalonia.Controls.Primitives
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Reset:
SelectedIndex = IndexOf(Items, SelectedItem);
if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0)
{
SelectedIndex = 0;
}
break;
}
}
@ -338,24 +347,36 @@ namespace Avalonia.Controls.Primitives
{
base.OnContainersMaterialized(e);
var selectedIndex = SelectedIndex;
var selectedContainer = e.Containers
.FirstOrDefault(x => (x.ContainerControl as ISelectable)?.IsSelected == true);
var resetSelectedItems = false;
if (selectedContainer != null)
foreach (var container in e.Containers)
{
SelectedIndex = selectedContainer.Index;
}
else if (selectedIndex >= e.StartingIndex &&
selectedIndex < e.StartingIndex + e.Containers.Count)
{
var container = e.Containers[selectedIndex - e.StartingIndex];
if ((container.ContainerControl as ISelectable)?.IsSelected == true)
{
if (SelectedIndex == -1)
{
SelectedIndex = container.Index;
}
else
{
if (_selection.Add(container.Index))
{
resetSelectedItems = true;
}
}
if (container.ContainerControl != null)
MarkContainerSelected(container.ContainerControl, true);
}
else if (_selection.Contains(container.Index))
{
MarkContainerSelected(container.ContainerControl, true);
}
}
if (resetSelectedItems)
{
ResetSelectedItems();
}
}
/// <inheritdoc/>
@ -469,11 +490,6 @@ namespace Avalonia.Controls.Primitives
/// </summary>
protected void SelectAll()
{
if ((SelectionMode & (SelectionMode.Multiple | SelectionMode.Toggle)) == 0)
{
throw new NotSupportedException("Multiple selection is not enabled on this control.");
}
UpdateSelectedItems(() =>
{
_selection.Clear();
@ -523,7 +539,14 @@ namespace Avalonia.Controls.Primitives
var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
var range = multi && rangeModifier;
if (range)
if (rightButton)
{
if (!_selection.Contains(index))
{
UpdateSelectedItem(index);
}
}
else if (range)
{
UpdateSelectedItems(() =>
{
@ -582,7 +605,7 @@ namespace Avalonia.Controls.Primitives
}
else
{
UpdateSelectedItem(index, !(rightButton && _selection.Contains(index)));
UpdateSelectedItem(index);
}
if (Presenter?.Panel != null)

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

@ -4,6 +4,7 @@
using Avalonia.Controls.Generators;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
namespace Avalonia.Controls.Primitives
{
@ -12,11 +13,8 @@ namespace Avalonia.Controls.Primitives
private static readonly FuncTemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new WrapPanel { Orientation = Orientation.Horizontal });
private static IMemberSelector s_MemberSelector = new FuncMemberSelector<object, object>(SelectHeader);
static TabStrip()
{
MemberSelectorProperty.OverrideDefaultValue<TabStrip>(s_MemberSelector);
SelectionModeProperty.OverrideDefaultValue<TabStrip>(SelectionMode.AlwaysSelected);
FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false);
ItemsPanelProperty.OverrideDefaultValue<TabStrip>(DefaultPanel);
@ -51,11 +49,5 @@ namespace Avalonia.Controls.Primitives
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
private static object SelectHeader(object o)
{
var headered = o as IHeadered;
return (headered != null) ? (headered.Header ?? string.Empty) : o;
}
}
}

1
src/Avalonia.Controls/Primitives/Track.cs

@ -3,6 +3,7 @@
using System;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Metadata;
namespace Avalonia.Controls.Primitives

7
src/Avalonia.Controls/ProgressBar.cs

@ -3,6 +3,7 @@
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
namespace Avalonia.Controls
{
@ -33,8 +34,8 @@ namespace Avalonia.Controls
static ProgressBar()
{
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
PseudoClass<ProgressBar>(IsIndeterminateProperty, ":indeterminate");
ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.UpdateIndicatorWhenPropChanged);
@ -120,4 +121,4 @@ namespace Avalonia.Controls
UpdateIndicator(Bounds.Size);
}
}
}
}

54
src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs

@ -0,0 +1,54 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using Avalonia.Controls.Templates;
namespace Avalonia.Controls
{
internal class ItemTemplateWrapper
{
private readonly IDataTemplate _dataTemplate;
public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate;
public IControl GetElement(IControl parent, object data)
{
var selectedTemplate = _dataTemplate;
var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate);
IControl element = null;
if (recyclePool != null)
{
// try to get an element from the recycle pool.
element = recyclePool.TryGetElement(string.Empty, parent);
}
if (element == null)
{
// no element was found in recycle pool, create a new element
element = selectedTemplate.Build(data);
// Associate template with element
element.SetValue(RecyclePool.OriginTemplateProperty, selectedTemplate);
}
return element;
}
public void RecycleElement(IControl parent, IControl element)
{
var selectedTemplate = _dataTemplate;
var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate);
if (recyclePool == null)
{
// No Recycle pool in the template, create one.
recyclePool = new RecyclePool();
RecyclePool.SetPoolInstance(selectedTemplate, recyclePool);
}
recyclePool.PutElement(element, "" /* key */, parent);
}
}
}

724
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -0,0 +1,724 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections;
using System.Collections.Specialized;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
namespace Avalonia.Controls
{
/// <summary>
/// Represents a data-driven collection control that incorporates a flexible layout system,
/// custom views, and virtualization.
/// </summary>
public class ItemsRepeater : Panel
{
/// <summary>
/// Defines the <see cref="HorizontalCacheLength"/> property.
/// </summary>
public static readonly AvaloniaProperty<double> HorizontalCacheLengthProperty =
AvaloniaProperty.Register<ItemsRepeater, double>(nameof(HorizontalCacheLength), 2.0);
/// <summary>
/// Defines the <see cref="ItemTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
ItemsControl.ItemTemplateProperty.AddOwner<ItemsRepeater>();
/// <summary>
/// Defines the <see cref="Items"/> property.
/// </summary>
public static readonly DirectProperty<ItemsRepeater, IEnumerable> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<ItemsRepeater>(o => o.Items, (o, v) => o.Items = v);
/// <summary>
/// Defines the <see cref="Layout"/> property.
/// </summary>
public static readonly AvaloniaProperty<AttachedLayout> LayoutProperty =
AvaloniaProperty.Register<ItemsRepeater, AttachedLayout>(nameof(Layout), new StackLayout());
/// <summary>
/// Defines the <see cref="VerticalCacheLength"/> property.
/// </summary>
public static readonly AvaloniaProperty<double> VerticalCacheLengthProperty =
AvaloniaProperty.Register<ItemsRepeater, double>(nameof(VerticalCacheLength), 2.0);
private static readonly AttachedProperty<VirtualizationInfo> VirtualizationInfoProperty =
AvaloniaProperty.RegisterAttached<ItemsRepeater, IControl, VirtualizationInfo>("VirtualizationInfo");
internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1);
internal static readonly Point ClearedElementsArrangePosition = new Point(-10000.0, -10000.0);
private readonly ViewManager _viewManager;
private readonly ViewportManager _viewportManager;
private IEnumerable _items;
private VirtualizingLayoutContext _layoutContext;
private NotifyCollectionChangedEventArgs _processingItemsSourceChange;
private bool _isLayoutInProgress;
private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs;
private ItemsRepeaterElementClearingEventArgs _elementClearingArgs;
private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs;
/// <summary>
/// Initializes a new instance of the <see cref="ItemsRepeater"/> class.
/// </summary>
public ItemsRepeater()
{
_viewManager = new ViewManager(this);
_viewportManager = new ViewportManager(this);
KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Once);
OnLayoutChanged(null, Layout);
}
static ItemsRepeater()
{
ClipToBoundsProperty.OverrideDefaultValue<ItemsRepeater>(true);
}
/// <summary>
/// Gets or sets the layout used to size and position elements in the ItemsRepeater.
/// </summary>
/// <value>
/// The layout used to size and position elements. The default is a StackLayout with
/// vertical orientation.
/// </value>
public AttachedLayout Layout
{
get => GetValue(LayoutProperty);
set => SetValue(LayoutProperty, value);
}
/// <summary>
/// Gets or sets an object source used to generate the content of the ItemsRepeater.
/// </summary>
public IEnumerable Items
{
get => _items;
set => SetAndRaise(ItemsProperty, ref _items, value);
}
/// <summary>
/// Gets or sets the template used to display each item.
/// </summary>
public IDataTemplate ItemTemplate
{
get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
/// <summary>
/// Gets or sets a value that indicates the size of the buffer used to realize items when
/// panning or scrolling horizontally.
/// </summary>
public double HorizontalCacheLength
{
get => GetValue(HorizontalCacheLengthProperty);
set => SetValue(HorizontalCacheLengthProperty, value);
}
/// <summary>
/// Gets or sets a value that indicates the size of the buffer used to realize items when
/// panning or scrolling vertically.
/// </summary>
public double VerticalCacheLength
{
get => GetValue(VerticalCacheLengthProperty);
set => SetValue(VerticalCacheLengthProperty, value);
}
/// <summary>
/// Gets a standardized view of the supported interactions between a given Items object and
/// the ItemsRepeater control and its associated components.
/// </summary>
public ItemsSourceView ItemsSourceView { get; private set; }
internal ItemTemplateWrapper ItemTemplateShim { get; set; }
internal Point LayoutOrigin { get; set; }
internal object LayoutState { get; set; }
internal IControl MadeAnchor => _viewportManager.MadeAnchor;
internal Rect RealizationWindow => _viewportManager.GetLayoutRealizationWindow();
internal IControl SuggestedAnchor => _viewportManager.SuggestedAnchor;
private bool IsProcessingCollectionChange => _processingItemsSourceChange != null;
private LayoutContext LayoutContext
{
get
{
if (_layoutContext == null)
{
_layoutContext = new RepeaterLayoutContext(this);
}
return _layoutContext;
}
}
/// <summary>
/// Occurs each time an element is cleared and made available to be re-used.
/// </summary>
/// <remarks>
/// This event is raised immediately each time an element is cleared, such as when it falls
/// outside the range of realized items. Elements are cleared when they become available
/// for re-use.
/// </remarks>
public event EventHandler<ItemsRepeaterElementClearingEventArgs> ElementClearing;
/// <summary>
/// Occurs for each realized <see cref="IControl"/> when the index for the item it
/// represents has changed.
/// </summary>
/// <remarks>
/// When you use ItemsRepeater to build a more complex control that supports specific
/// interactions on the child elements (such as selection or click), it is useful to be
/// able to keep an up-to-date identifier for the backing data item.
///
/// This event is raised for each realized IControl where the index for the item it
/// represents has changed. For example, when another item is added or removed in the data
/// source, the index for items that come after in the ordering will be impacted.
/// </remarks>
public event EventHandler<ItemsRepeaterElementIndexChangedEventArgs> ElementIndexChanged;
/// <summary>
/// Occurs each time an element is prepared for use.
/// </summary>
/// <remarks>
/// The prepared element might be newly created or an existing element that is being re-
/// used.
/// </remarks>
public event EventHandler<ItemsRepeaterElementPreparedEventArgs> ElementPrepared;
/// <summary>
/// Retrieves the index of the item from the data source that corresponds to the specified
/// <see cref="IControl"/>.
/// </summary>
/// <param name="element">
/// The element that corresponds to the item to get the index of.
/// </param>
/// <returns>
/// The index of the item from the data source that corresponds to the specified UIElement,
/// or -1 if the element is not supported.
/// </returns>
public int GetElementIndex(IControl element) => GetElementIndexImpl(element);
/// <summary>
/// Retrieves the realized UIElement that corresponds to the item at the specified index in
/// the data source.
/// </summary>
/// <param name="index">The index of the item.</param>
/// <returns>
/// he UIElement that corresponds to the item at the specified index if the item is
/// realized, or null if the item is not realized.
/// </returns>
public IControl TryGetElement(int index) => GetElementFromIndexImpl(index);
internal void PinElement(IControl element) => _viewManager.UpdatePin(element, true);
internal void UnpinElement(IControl element) => _viewManager.UpdatePin(element, false);
internal IControl GetOrCreateElement(int index) => GetOrCreateElementImpl(index);
internal static VirtualizationInfo TryGetVirtualizationInfo(IControl element)
{
var value = element.GetValue(VirtualizationInfoProperty);
return value;
}
internal static VirtualizationInfo CreateAndInitializeVirtualizationInfo(IControl element)
{
if (TryGetVirtualizationInfo(element) != null)
{
throw new InvalidOperationException("VirtualizationInfo already created.");
}
var result = new VirtualizationInfo();
element.SetValue(VirtualizationInfoProperty, result);
return result;
}
internal static VirtualizationInfo GetVirtualizationInfo(IControl element)
{
var result = element.GetValue(VirtualizationInfoProperty);
if (result == null)
{
result = new VirtualizationInfo();
element.SetValue(VirtualizationInfoProperty, result);
}
return result;
}
protected override Size MeasureOverride(Size availableSize)
{
if (_isLayoutInProgress)
{
throw new AvaloniaInternalException("Reentrancy detected during layout.");
}
if (IsProcessingCollectionChange)
{
throw new NotSupportedException("Cannot run layout in the middle of a collection change.");
}
_viewportManager.OnOwnerMeasuring();
_isLayoutInProgress = true;
try
{
_viewManager.PrunePinnedElements();
var extent = new Rect();
var desiredSize = new Size();
var layout = Layout;
if (layout != null)
{
var layoutContext = GetLayoutContext();
desiredSize = layout.Measure(layoutContext, availableSize);
extent = new Rect(LayoutOrigin.X, LayoutOrigin.Y, desiredSize.Width, desiredSize.Height);
// Clear auto recycle candidate elements that have not been kept alive by layout - i.e layout did not
// call GetElementAt(index).
foreach (var element in Children)
{
var virtInfo = GetVirtualizationInfo(element);
if (virtInfo.Owner == ElementOwner.Layout &&
virtInfo.AutoRecycleCandidate &&
!virtInfo.KeepAlive)
{
ClearElementImpl(element);
}
}
}
_viewportManager.SetLayoutExtent(extent);
return desiredSize;
}
finally
{
_isLayoutInProgress = false;
}
}
protected override Size ArrangeOverride(Size finalSize)
{
if (_isLayoutInProgress)
{
throw new AvaloniaInternalException("Reentrancy detected during layout.");
}
if (IsProcessingCollectionChange)
{
throw new NotSupportedException("Cannot run layout in the middle of a collection change.");
}
_isLayoutInProgress = true;
try
{
var arrangeSize = Layout?.Arrange(GetLayoutContext(), finalSize) ?? default;
// The view manager might clear elements during this call.
// That's why we call it before arranging cleared elements
// off screen.
_viewManager.OnOwnerArranged();
foreach (var element in Children)
{
var virtInfo = GetVirtualizationInfo(element);
virtInfo.KeepAlive = false;
if (virtInfo.Owner == ElementOwner.ElementFactory ||
virtInfo.Owner == ElementOwner.PinnedPool)
{
// Toss it away. And arrange it with size 0 so that XYFocus won't use it.
element.Arrange(new Rect(
ClearedElementsArrangePosition.X - element.DesiredSize.Width,
ClearedElementsArrangePosition.Y - element.DesiredSize.Height,
0,
0));
}
else
{
var newBounds = element.Bounds;
virtInfo.ArrangeBounds = newBounds;
}
}
_viewportManager.OnOwnerArranged();
return arrangeSize;
}
finally
{
_isLayoutInProgress = false;
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
InvalidateMeasure();
_viewportManager.ResetScrollers();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_viewportManager.ResetScrollers();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
{
var property = args.Property;
if (property == ItemsProperty)
{
var newValue = (IEnumerable)args.NewValue;
var newDataSource = newValue as ItemsSourceView;
if (newValue != null && newDataSource == null)
{
newDataSource = new ItemsSourceView(newValue);
}
OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
}
else if (property == ItemTemplateProperty)
{
OnItemTemplateChanged((IDataTemplate)args.OldValue, (IDataTemplate)args.NewValue);
}
else if (property == LayoutProperty)
{
OnLayoutChanged((AttachedLayout)args.OldValue, (AttachedLayout)args.NewValue);
}
else if (property == HorizontalCacheLengthProperty)
{
_viewportManager.HorizontalCacheLength = (double)args.NewValue;
}
else if (property == VerticalCacheLengthProperty)
{
_viewportManager.VerticalCacheLength = (double)args.NewValue;
}
else
{
base.OnPropertyChanged(args);
}
}
internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle)
{
var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle);
return element;
}
internal void ClearElementImpl(IControl element)
{
// Clearing an element due to a collection change
// is more strict in that pinned elements will be forcibly
// unpinned and sent back to the view generator.
var isClearedDueToCollectionChange =
_processingItemsSourceChange != null &&
(_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Remove ||
_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Replace ||
_processingItemsSourceChange.Action == NotifyCollectionChangedAction.Reset);
_viewManager.ClearElement(element, isClearedDueToCollectionChange);
_viewportManager.OnElementCleared(element);
}
private int GetElementIndexImpl(IControl element)
{
var virtInfo = TryGetVirtualizationInfo(element);
return _viewManager.GetElementIndex(virtInfo);
}
private IControl GetElementFromIndexImpl(int index)
{
IControl result = null;
var children = Children;
for (var i = 0; i < children.Count && result == null; ++i)
{
var element = children[i];
var virtInfo = TryGetVirtualizationInfo(element);
if (virtInfo?.IsRealized == true && virtInfo.Index == index)
{
result = element;
}
}
return result;
}
private IControl GetOrCreateElementImpl(int index)
{
if (index >= 0 && index >= ItemsSourceView.Count)
{
throw new ArgumentException("Argument index is invalid.", "index");
}
if (_isLayoutInProgress)
{
throw new NotSupportedException("GetOrCreateElement invocation is not allowed during layout.");
}
var element = GetElementFromIndexImpl(index);
bool isAnchorOutsideRealizedRange = element == null;
if (isAnchorOutsideRealizedRange)
{
if (Layout == null)
{
throw new InvalidOperationException("Cannot make an Anchor when there is no attached layout.");
}
element = (IControl)GetLayoutContext().GetOrCreateElementAt(index);
element.Measure(Size.Infinity);
}
_viewportManager.OnMakeAnchor(element, isAnchorOutsideRealizedRange);
InvalidateMeasure();
return element;
}
internal void OnElementPrepared(IControl element, int index)
{
_viewportManager.OnElementPrepared(element);
if (ElementPrepared != null)
{
if (_elementPreparedArgs == null)
{
_elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index);
}
else
{
_elementPreparedArgs.Update(element, index);
}
ElementPrepared(this, _elementPreparedArgs);
}
}
internal void OnElementClearing(IControl element)
{
if (ElementClearing != null)
{
if (_elementClearingArgs == null)
{
_elementClearingArgs = new ItemsRepeaterElementClearingEventArgs(element);
}
else
{
_elementClearingArgs.Update(element);
}
ElementClearing(this, _elementClearingArgs);
}
}
internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex)
{
if (ElementIndexChanged != null)
{
if (_elementIndexChangedArgs == null)
{
_elementIndexChangedArgs = new ItemsRepeaterElementIndexChangedEventArgs(element, oldIndex, newIndex);
}
else
{
_elementIndexChangedArgs.Update(element, oldIndex, newIndex);
}
ElementIndexChanged(this, _elementIndexChangedArgs);
}
}
private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue)
{
if (_isLayoutInProgress)
{
throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout.");
}
ItemsSourceView?.Dispose();
ItemsSourceView = newValue;
if (oldValue != null)
{
oldValue.CollectionChanged -= OnItemsSourceViewChanged;
}
if (newValue != null)
{
newValue.CollectionChanged += OnItemsSourceViewChanged;
}
if (Layout != null)
{
if (Layout is VirtualizingLayout virtualLayout)
{
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args);
}
else if (Layout is NonVirtualizingLayout nonVirtualLayout)
{
// Walk through all the elements and make sure they are cleared for
// non-virtualizing layouts.
foreach (var element in Children)
{
if (GetVirtualizationInfo(element).IsRealized)
{
ClearElementImpl(element);
}
}
}
InvalidateMeasure();
}
}
private void OnItemTemplateChanged(IDataTemplate oldValue, IDataTemplate newValue)
{
if (_isLayoutInProgress && oldValue != null)
{
throw new AvaloniaInternalException("ItemTemplate cannot be changed during layout.");
}
// Since the ItemTemplate has changed, we need to re-evaluate all the items that
// have already been created and are now in the tree. The easiest way to do that
// would be to do a reset.. Note that this has to be done before we change the template
// so that the cleared elements go back into the old template.
if (Layout != null)
{
if (Layout is VirtualizingLayout virtualLayout)
{
var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
_processingItemsSourceChange = args;
try
{
virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args);
}
finally
{
_processingItemsSourceChange = null;
}
}
else if (Layout is NonVirtualizingLayout)
{
// Walk through all the elements and make sure they are cleared for
// non-virtualizing layouts.
foreach (var element in Children)
{
if (GetVirtualizationInfo(element).IsRealized)
{
ClearElementImpl(element);
}
}
}
}
ItemTemplateShim = new ItemTemplateWrapper(newValue);
InvalidateMeasure();
}
private void OnLayoutChanged(AttachedLayout oldValue, AttachedLayout newValue)
{
if (_isLayoutInProgress)
{
throw new InvalidOperationException("Layout cannot be changed during layout.");
}
_viewManager.OnLayoutChanging();
if (oldValue != null)
{
oldValue.UninitializeForContext(LayoutContext);
oldValue.MeasureInvalidated -= InvalidateMeasureForLayout;
oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout;
// Walk through all the elements and make sure they are cleared
foreach (var element in Children)
{
if (GetVirtualizationInfo(element).IsRealized)
{
ClearElementImpl(element);
}
}
LayoutState = null;
}
if (newValue != null)
{
newValue.InitializeForContext(LayoutContext);
newValue.MeasureInvalidated += InvalidateMeasureForLayout;
newValue.ArrangeInvalidated += InvalidateArrangeForLayout;
}
bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout;
_viewportManager.OnLayoutChanged(isVirtualizingLayout);
InvalidateMeasure();
}
private void OnItemsSourceViewChanged(object sender, NotifyCollectionChangedEventArgs args)
{
if (_isLayoutInProgress)
{
// Bad things will follow if the data changes while we are in the middle of a layout pass.
throw new InvalidOperationException("Changes in data source are not allowed during layout.");
}
if (IsProcessingCollectionChange)
{
throw new InvalidOperationException("Changes in the data source are not allowed during another change in the data source.");
}
_processingItemsSourceChange = args;
try
{
_viewManager.OnItemsSourceChanged(sender, args);
if (Layout != null)
{
if (Layout is VirtualizingLayout virtualLayout)
{
virtualLayout.OnItemsChanged(GetLayoutContext(), sender, args);
}
else
{
// NonVirtualizingLayout
InvalidateMeasure();
}
}
}
finally
{
_processingItemsSourceChange = null;
}
}
private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateMeasure();
private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateArrange();
private VirtualizingLayoutContext GetLayoutContext()
{
if (_layoutContext == null)
{
_layoutContext = new RepeaterLayoutContext(this);
}
return _layoutContext;
}
}
}

24
src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs

@ -0,0 +1,24 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="ItemsRepeater.ElementClearing"/> event.
/// </summary>
public class ItemsRepeaterElementClearingEventArgs : EventArgs
{
internal ItemsRepeaterElementClearingEventArgs(IControl element) => Element = element;
/// <summary>
/// Gets the element that is being cleared for re-use.
/// </summary>
public IControl Element { get; private set; }
internal void Update(IControl element) => Element = element;
}
}

44
src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs

@ -0,0 +1,44 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="ItemsRepeater.ElementIndexChanged"/> event.
/// </summary>
public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs
{
internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex)
{
Element = element;
NewIndex = newIndex;
OldIndex = oldIndex;
}
/// <summary>
/// Get the element for which the index changed.
/// </summary>
public IControl Element { get; private set; }
/// <summary>
/// Gets the index of the element after the change.
/// </summary>
public int NewIndex { get; private set; }
/// <summary>
/// Gets the index of the element before the change.
/// </summary>
public int OldIndex { get; private set; }
internal void Update(IControl element, int newIndex, int oldIndex)
{
Element = element;
NewIndex = newIndex;
OldIndex = oldIndex;
}
}
}

35
src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs

@ -0,0 +1,35 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="ItemsRepeater.ElementPrepared"/> event.
/// </summary>
public class ItemsRepeaterElementPreparedEventArgs
{
internal ItemsRepeaterElementPreparedEventArgs(IControl element, int index)
{
Element = element;
Index = index;
}
/// <summary>
/// Gets the prepared element.
/// </summary>
public IControl Element { get; private set; }
/// <summary>
/// Gets the index of the item the element was prepared for.
/// </summary>
public int Index { get; private set; }
internal void Update(IControl element, int index)
{
Element = element;
Index = index;
}
}
}

143
src/Avalonia.Controls/Repeater/ItemsSourceView.cs

@ -0,0 +1,143 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace Avalonia.Controls
{
/// <summary>
/// Represents a standardized view of the supported interactions between a given ItemsSource
/// object and an <see cref="ItemsRepeater"/> control.
/// </summary>
/// <remarks>
/// Components written to work with ItemsRepeater should consume the
/// <see cref="ItemsRepeater.Items"/> via ItemsSourceView since this provides a normalized
/// view of the Items. That way, each component does not need to know if the source is an
/// IEnumerable, an IList, or something else.
/// </remarks>
public class ItemsSourceView : INotifyCollectionChanged, IDisposable
{
private readonly IList _inner;
private INotifyCollectionChanged _notifyCollectionChanged;
private int _cachedSize = -1;
/// <summary>
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
/// </summary>
/// <param name="source">The data source.</param>
public ItemsSourceView(IEnumerable source)
{
Contract.Requires<ArgumentNullException>(source != null);
_inner = source as IList;
if (_inner == null && source is IEnumerable<object> objectEnumerable)
{
_inner = new List<object>(objectEnumerable);
}
else
{
_inner = new List<object>(source.Cast<object>());
}
ListenToCollectionChanges();
}
/// <summary>
/// Gets the number of items in the collection.
/// </summary>
public int Count
{
get
{
if (_cachedSize == -1)
{
_cachedSize = _inner.Count;
}
return _cachedSize;
}
}
/// <summary>
/// Gets a value that indicates whether the items source can provide a unique key for each item.
/// </summary>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
public bool HasKeyIndexMapping => false;
/// <summary>
/// Occurs when the collection has changed to indicate the reason for the change and which items changed.
/// </summary>
public event NotifyCollectionChangedEventHandler CollectionChanged;
/// <inheritdoc/>
public void Dispose()
{
if (_notifyCollectionChanged != null)
{
_notifyCollectionChanged.CollectionChanged -= OnCollectionChanged;
}
}
/// <summary>
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>the item.</returns>
public object GetAt(int index) => _inner[index];
/// <summary>
/// Retrieves the index of the item that has the specified unique identifier (key).
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The key</returns>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
public string KeyFromIndex(int index)
{
throw new NotImplementedException();
}
/// <summary>
/// Retrieves the unique identifier (key) for the item at the specified index.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>The index.</returns>
/// <remarks>
/// TODO: Not yet implemented in Avalonia.
/// </remarks>
public int IndexFromKey(string key)
{
throw new NotImplementedException();
}
protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args)
{
_cachedSize = _inner.Count;
CollectionChanged?.Invoke(this, args);
}
private void ListenToCollectionChanges()
{
if (_inner is INotifyCollectionChanged incc)
{
incc.CollectionChanged += OnCollectionChanged;
_notifyCollectionChanged = incc;
}
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
OnItemsSourceChanged(e);
}
}
}

106
src/Avalonia.Controls/Repeater/RecyclePool.cs

@ -0,0 +1,106 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Controls.Templates;
namespace Avalonia.Controls
{
internal class RecyclePool
{
public static readonly AttachedProperty<IDataTemplate> OriginTemplateProperty =
AvaloniaProperty.RegisterAttached<Control, IDataTemplate>("OriginTemplate", typeof(RecyclePool));
private static ConditionalWeakTable<IDataTemplate, RecyclePool> s_pools = new ConditionalWeakTable<IDataTemplate, RecyclePool>();
private readonly Dictionary<string, List<ElementInfo>> _elements = new Dictionary<string, List<ElementInfo>>();
public static RecyclePool GetPoolInstance(IDataTemplate dataTemplate)
{
s_pools.TryGetValue(dataTemplate, out var result);
return result;
}
public static void SetPoolInstance(IDataTemplate dataTemplate, RecyclePool value) => s_pools.Add(dataTemplate, value);
public void PutElement(IControl element, string key, IControl owner)
{
var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner);
var elementInfo = new ElementInfo(element, ownerAsPanel);
if (!_elements.TryGetValue(key, out var pool))
{
pool = new List<ElementInfo>();
_elements.Add(key, pool);
}
pool.Add(elementInfo);
}
public IControl TryGetElement(string key, IControl owner)
{
if (_elements.TryGetValue(key, out var elements))
{
if (elements.Count > 0)
{
// Prefer an element from the same owner or with no owner so that we don't incur
// the enter/leave cost during recycling.
// TODO: prioritize elements with the same owner to those without an owner.
var elementInfo = elements.FirstOrDefault(x => x.Owner == owner) ?? elements.LastOrDefault();
elements.Remove(elementInfo);
var ownerAsPanel = EnsureOwnerIsPanelOrNull(owner);
if (elementInfo.Owner != null && elementInfo.Owner != ownerAsPanel)
{
// Element is still under its parent. remove it from its parent.
var panel = elementInfo.Owner;
if (panel != null)
{
int childIndex = panel.Children.IndexOf(elementInfo.Element);
if (childIndex == -1)
{
throw new KeyNotFoundException("ItemsRepeater's child not found in its Children collection.");
}
panel.Children.RemoveAt(childIndex);
}
}
return elementInfo.Element;
}
}
return null;
}
private IPanel EnsureOwnerIsPanelOrNull(IControl owner)
{
if (owner is IPanel panel)
{
return panel;
}
else if (owner != null)
{
throw new InvalidOperationException("Owner must be IPanel or null.");
}
return null;
}
private class ElementInfo
{
public ElementInfo(IControl element, IPanel owner)
{
Element = element;
Owner = owner;
}
public IControl Element { get; }
public IPanel Owner { get;}
}
}
}

65
src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs

@ -0,0 +1,65 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Layout;
namespace Avalonia.Controls
{
internal class RepeaterLayoutContext : VirtualizingLayoutContext
{
private readonly ItemsRepeater _owner;
public RepeaterLayoutContext(ItemsRepeater owner)
{
_owner = owner;
}
protected override Point LayoutOriginCore
{
get => _owner.LayoutOrigin;
set => _owner.LayoutOrigin = value;
}
protected override object LayoutStateCore
{
get => _owner.LayoutState;
set => _owner.LayoutState = value;
}
protected override int RecommendedAnchorIndexCore
{
get
{
int anchorIndex = -1;
var anchor = _owner.SuggestedAnchor;
if (anchor != null)
{
anchorIndex = _owner.GetElementIndex(anchor);
}
return anchorIndex;
}
}
protected override int ItemCountCore() => _owner.ItemsSourceView?.Count ?? 0;
protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options)
{
return _owner.GetElementImpl(
index,
(options & ElementRealizationOptions.ForceCreate) != 0,
(options & ElementRealizationOptions.SuppressAutoRecycle) != 0);
}
protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index);
protected override void RecycleElementCore(ILayoutable element) => _owner.ClearElementImpl((IControl)element);
protected override Rect RealizationRectCore() => _owner.RealizationWindow;
}
}

54
src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs

@ -0,0 +1,54 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Controls
{
internal class UniqueIdElementPool : IEnumerable<KeyValuePair<string, IControl>>
{
private readonly Dictionary<string, IControl> _elementMap = new Dictionary<string, IControl>();
private readonly ItemsRepeater _owner;
public UniqueIdElementPool(ItemsRepeater owner) => _owner = owner;
public void Add(IControl element)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var key = virtInfo.UniqueId;
if (_elementMap.ContainsKey(key))
{
throw new InvalidOperationException($"The unique id provided ({key}) is not unique.");
}
_elementMap.Add(key, element);
}
public IControl Remove(int index)
{
// Check if there is already a element in the mapping and if so, use it.
string key = _owner.ItemsSourceView.KeyFromIndex(index);
if (_elementMap.TryGetValue(key, out var element))
{
_elementMap.Remove(key);
}
return element;
}
public void Clear()
{
_elementMap.Clear();
}
public IEnumerator<KeyValuePair<string, IControl>> GetEnumerator() => _elementMap.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

682
src/Avalonia.Controls/Repeater/ViewManager.cs

@ -0,0 +1,682 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
internal sealed class ViewManager
{
private const int FirstRealizedElementIndexDefault = int.MaxValue;
private const int LastRealizedElementIndexDefault = int.MinValue;
private readonly ItemsRepeater _owner;
private readonly List<PinnedElementInfo> _pinnedPool = new List<PinnedElementInfo>();
private readonly UniqueIdElementPool _resetPool;
private IControl _lastFocusedElement;
private bool _isDataSourceStableResetPending;
private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault;
private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault;
private bool _eventsSubscribed;
public ViewManager(ItemsRepeater owner)
{
_owner = owner;
_resetPool = new UniqueIdElementPool(owner);
}
public IControl GetElement(int index, bool forceCreate, bool suppressAutoRecycle)
{
var element = forceCreate ? null : GetElementIfAlreadyHeldByLayout(index);
if (element == null)
{
// check if this is the anchor made through repeater in preparation
// for a bring into view.
var madeAnchor = _owner.MadeAnchor;
if (madeAnchor != null)
{
var anchorVirtInfo = ItemsRepeater.TryGetVirtualizationInfo(madeAnchor);
if (anchorVirtInfo.Index == index)
{
element = madeAnchor;
}
}
}
if (element == null) { element = GetElementFromUniqueIdResetPool(index); };
if (element == null) { element = GetElementFromPinnedElements(index); }
if (element == null) { element = GetElementFromElementFactory(index); }
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
if (suppressAutoRecycle)
{
virtInfo.AutoRecycleCandidate = false;
}
else
{
virtInfo.AutoRecycleCandidate = true;
virtInfo.KeepAlive = true;
}
return element;
}
public void ClearElement(IControl element, bool isClearedDueToCollectionChange)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var index = virtInfo.Index;
bool cleared =
ClearElementToUniqueIdResetPool(element, virtInfo) ||
ClearElementToPinnedPool(element, virtInfo, isClearedDueToCollectionChange);
if (!cleared)
{
ClearElementToElementFactory(element);
}
// Both First and Last indices need to be valid or default.
if (index == _firstRealizedElementIndexHeldByLayout && index == _lastRealizedElementIndexHeldByLayout)
{
// First and last were pointing to the same element and that is going away.
InvalidateRealizedIndicesHeldByLayout();
}
else if (index == _firstRealizedElementIndexHeldByLayout)
{
// The FirstElement is going away, shrink the range by one.
++_firstRealizedElementIndexHeldByLayout;
}
else if (index == _lastRealizedElementIndexHeldByLayout)
{
// Last element is going away, shrink the range by one at the end.
--_lastRealizedElementIndexHeldByLayout;
}
else
{
// Index is either outside the range we are keeping track of or inside the range.
// In both these cases, we just keep the range we have. If this clear was due to
// a collection change, then in the CollectionChanged event, we will invalidate these guys.
}
}
public void ClearElementToElementFactory(IControl element)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var clearedIndex = virtInfo.Index;
_owner.OnElementClearing(element);
_owner.ItemTemplateShim.RecycleElement(_owner, element);
virtInfo.MoveOwnershipToElementFactory();
if (_lastFocusedElement == element)
{
// Focused element is going away. Remove the tracked last focused element
// and pick a reasonable next focus if we can find one within the layout
// realized elements.
MoveFocusFromClearedIndex(clearedIndex);
}
}
private void MoveFocusFromClearedIndex(int clearedIndex)
{
IControl focusedChild = null;
var focusCandidate = FindFocusCandidate(clearedIndex, focusedChild);
if (focusCandidate != null)
{
focusCandidate.Focus();
_lastFocusedElement = focusedChild;
// Add pin to hold the focused element.
UpdatePin(focusedChild, true /* addPin */);
}
else
{
// We could not find a candiate.
_lastFocusedElement = null;
}
}
IControl FindFocusCandidate(int clearedIndex, IControl focusedChild)
{
// Walk through all the children and find elements with index before and after the cleared index.
// Note that during a delete the next element would now have the same index.
int previousIndex = int.MinValue;
int nextIndex = int.MaxValue;
IControl nextElement = null;
IControl previousElement = null;
foreach (var child in _owner.Children)
{
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child);
if (virtInfo?.IsHeldByLayout == true)
{
int currentIndex = virtInfo.Index;
if (currentIndex < clearedIndex)
{
if (currentIndex > previousIndex)
{
previousIndex = currentIndex;
previousElement = child;
}
}
else if (currentIndex >= clearedIndex)
{
// Note that we use >= above because if we deleted the focused element,
// the next element would have the same index now.
if (currentIndex < nextIndex)
{
nextIndex = currentIndex;
nextElement = child;
}
}
}
}
// TODO: Find the next element if one exists, if not use the previous element.
// If the container itself is not focusable, find a descendent that is.
return nextElement;
}
public int GetElementIndex(VirtualizationInfo virtInfo)
{
if (virtInfo == null)
{
throw new ArgumentException("Element is not a child of this ItemsRepeater.");
}
return virtInfo.IsRealized || virtInfo.IsInUniqueIdResetPool ? virtInfo.Index : -1;
}
public void PrunePinnedElements()
{
EnsureEventSubscriptions();
// Go through pinned elements and make sure they still have
// a reason to be pinned.
for (var i = 0; i < _pinnedPool.Count; ++i)
{
var elementInfo = _pinnedPool[i];
var virtInfo = elementInfo.VirtualizationInfo;
if (!virtInfo.IsPinned)
{
_pinnedPool.RemoveAt(i);
--i;
// Pinning was the only thing keeping this element alive.
ClearElementToElementFactory(elementInfo.PinnedElement);
}
}
}
public void UpdatePin(IControl element, bool addPin)
{
var parent = element.VisualParent;
var child = (IVisual)element;
while (parent != null)
{
if (parent is ItemsRepeater repeater)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo((IControl)child);
if (virtInfo.IsRealized)
{
if (addPin)
{
virtInfo.AddPin();
}
else if (virtInfo.IsPinned)
{
if (virtInfo.RemovePin() == 0)
{
// ElementFactory is invoked during the measure pass.
// We will clear the element then.
repeater.InvalidateMeasure();
}
}
}
}
child = parent;
parent = child.VisualParent;
}
}
public void OnItemsSourceChanged(object sender, NotifyCollectionChangedEventArgs args)
{
// Note: For items that have been removed, the index will not be touched. It will hold
// the old index before it was removed. It is not valid anymore.
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
{
var newIndex = args.NewStartingIndex;
var newCount = args.NewItems.Count;
EnsureFirstLastRealizedIndices();
if (newIndex <= _lastRealizedElementIndexHeldByLayout)
{
_lastRealizedElementIndexHeldByLayout += newCount;
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized && dataIndex >= newIndex)
{
UpdateElementIndex(element, virtInfo, dataIndex + newCount);
}
}
}
else
{
// Indices held by layout are not affected
// We could still have items in the pinned elements that need updates. This is usually a very small vector.
for (var i = 0; i < _pinnedPool.Count; ++i)
{
var elementInfo = _pinnedPool[i];
var virtInfo = elementInfo.VirtualizationInfo;
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized && dataIndex >= newIndex)
{
var element = elementInfo.PinnedElement;
UpdateElementIndex(element, virtInfo, dataIndex + newCount);
}
}
}
break;
}
case NotifyCollectionChangedAction.Replace:
{
// Requirement: oldStartIndex == newStartIndex. It is not a replace if this is not true.
// Two cases here
// case 1: oldCount == newCount
// indices are not affected. nothing to do here.
// case 2: oldCount != newCount
// Replaced with less or more items. This is like an insert or remove
// depending on the counts.
var oldStartIndex = args.OldStartingIndex;
var newStartingIndex = args.NewStartingIndex;
var oldCount = args.OldItems.Count;
var newCount = args.NewItems.Count;
if (oldStartIndex != newStartingIndex)
{
throw new NotSupportedException("Replace is only allowed with OldStartingIndex equals to NewStartingIndex.");
}
if (oldCount == 0)
{
throw new NotSupportedException("Replace notification with args.OldItemsCount value of 0 is not allowed. Use Insert action instead.");
}
if (newCount == 0)
{
throw new NotSupportedException("Replace notification with args.NewItemCount value of 0 is not allowed. Use Remove action instead.");
}
int countChange = newCount - oldCount;
if (countChange != 0)
{
// countChange > 0 : countChange items were added
// countChange < 0 : -countChange items were removed
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized)
{
if (dataIndex >= oldStartIndex + oldCount)
{
UpdateElementIndex(element, virtInfo, dataIndex + countChange);
}
}
}
EnsureFirstLastRealizedIndices();
_lastRealizedElementIndexHeldByLayout += countChange;
}
break;
}
case NotifyCollectionChangedAction.Remove:
{
var oldStartIndex = args.OldStartingIndex;
var oldCount = args.OldItems.Count;
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
var dataIndex = virtInfo.Index;
if (virtInfo.IsRealized)
{
if (virtInfo.AutoRecycleCandidate && oldStartIndex <= dataIndex && dataIndex < oldStartIndex + oldCount)
{
// If we are doing the mapping, remove the element who's data was removed.
_owner.ClearElementImpl(element);
}
else if (dataIndex >= (oldStartIndex + oldCount))
{
UpdateElementIndex(element, virtInfo, dataIndex - oldCount);
}
}
}
InvalidateRealizedIndicesHeldByLayout();
break;
}
case NotifyCollectionChangedAction.Reset:
if (_owner.ItemsSourceView.HasKeyIndexMapping)
{
_isDataSourceStableResetPending = true;
}
// Walk through all the elements and make sure they are cleared, they will go into
// the stable id reset pool.
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
{
_owner.ClearElementImpl(element);
}
}
InvalidateRealizedIndicesHeldByLayout();
break;
}
}
private void EnsureFirstLastRealizedIndices()
{
if (_firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault)
{
// This will ensure that the indexes are updated.
GetElementIfAlreadyHeldByLayout(0);
}
}
public void OnLayoutChanging()
{
if (_owner.ItemsSourceView?.HasKeyIndexMapping == true)
{
_isDataSourceStableResetPending = true;
}
}
public void OnOwnerArranged()
{
if (_isDataSourceStableResetPending)
{
_isDataSourceStableResetPending = false;
foreach (var entry in _resetPool)
{
// TODO: Task 14204306: ItemsRepeater: Find better focus candidate when focused element is deleted in the ItemsSource.
// Focused element is getting cleared. Need to figure out semantics on where
// focus should go when the focused element is removed from the data collection.
ClearElement(entry.Value, true /* isClearedDueToCollectionChange */);
}
_resetPool.Clear();
}
}
// We optimize for the case where index is not realized to return null as quickly as we can.
// Flow layouts manage containers on their own and will never ask for an index that is already realized.
// If an index that is realized is requested by the layout, we unfortunately have to walk the
// children. Not ideal, but a reasonable default to provide consistent behavior between virtualizing
// and non-virtualizing hosts.
private IControl GetElementIfAlreadyHeldByLayout(int index)
{
IControl element = null;
bool cachedFirstLastIndicesInvalid = _firstRealizedElementIndexHeldByLayout == FirstRealizedElementIndexDefault;
bool isRequestedIndexInRealizedRange = (_firstRealizedElementIndexHeldByLayout <= index && index <= _lastRealizedElementIndexHeldByLayout);
if (cachedFirstLastIndicesInvalid || isRequestedIndexInRealizedRange)
{
foreach (var child in _owner.Children)
{
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child);
if (virtInfo?.IsHeldByLayout == true)
{
// Only give back elements held by layout. If someone else is holding it, they will be served by other methods.
int childIndex = virtInfo.Index;
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, childIndex);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, childIndex);
if (virtInfo.Index == index)
{
element = child;
// If we have valid first/last indices, we don't have to walk the rest, but if we
// do not, then we keep walking through the entire children collection to get accurate
// indices once.
if (!cachedFirstLastIndicesInvalid)
{
break;
}
}
}
}
}
return element;
}
private IControl GetElementFromUniqueIdResetPool(int index)
{
IControl element = null;
// See if you can get it from the reset pool.
if (_isDataSourceStableResetPending)
{
element = _resetPool.Remove(index);
if (element != null)
{
// Make sure that the index is updated to the current one
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool();
UpdateElementIndex(element, virtInfo, index);
}
}
return element;
}
private IControl GetElementFromPinnedElements(int index)
{
IControl element = null;
// See if you can find something among the pinned elements.
for (var i = 0; i < _pinnedPool.Count; ++i)
{
var elementInfo = _pinnedPool[i];
var virtInfo = elementInfo.VirtualizationInfo;
if (virtInfo.Index == index)
{
_pinnedPool.RemoveAt(i);
element = elementInfo.PinnedElement;
elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool();
break;
}
}
return element;
}
private IControl GetElementFromElementFactory(int index)
{
// The view generator is the provider of last resort.
var itemTemplateFactory = _owner.ItemTemplateShim;
if (itemTemplateFactory == null)
{
// If no ItemTemplate was provided, use a default
var factory = FuncDataTemplate.Default;
_owner.ItemTemplate = factory;
itemTemplateFactory = _owner.ItemTemplateShim;
}
var data = _owner.ItemsSourceView.GetAt(index);
var element = itemTemplateFactory.GetElement(_owner, data);
var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element);
if (virtInfo == null)
{
virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element);
}
// Prepare the element
element.DataContext = data;
virtInfo.MoveOwnershipToLayoutFromElementFactory(
index,
/* uniqueId: */
_owner.ItemsSourceView.HasKeyIndexMapping ?
_owner.ItemsSourceView.KeyFromIndex(index) :
string.Empty);
// The view generator is the only provider that prepares the element.
var repeater = _owner;
// Add the element to the children collection here before raising OnElementPrepared so
// that handlers can walk up the tree in case they want to find their IndexPath in the
// nested case.
var children = repeater.Children;
if (element.VisualParent != repeater)
{
children.Add(element);
}
repeater.OnElementPrepared(element, index);
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
return element;
}
private bool ClearElementToUniqueIdResetPool(IControl element, VirtualizationInfo virtInfo)
{
if (_isDataSourceStableResetPending)
{
_resetPool.Add(element);
virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout();
}
return _isDataSourceStableResetPending;
}
private bool ClearElementToPinnedPool(IControl element, VirtualizationInfo virtInfo, bool isClearedDueToCollectionChange)
{
if (_isDataSourceStableResetPending)
{
_resetPool.Add(element);
virtInfo.MoveOwnershipToUniqueIdResetPoolFromLayout();
}
return _isDataSourceStableResetPending;
}
private void UpdateFocusedElement()
{
IControl focusedElement = null;
var child = FocusManager.Instance.Current;
if (child != null)
{
var parent = child.VisualParent;
var owner = _owner;
// Find out if the focused element belongs to one of our direct
// children.
while (parent != null)
{
if (parent is ItemsRepeater repeater)
{
var element = child as IControl;
if (repeater == owner && ItemsRepeater.GetVirtualizationInfo(element).IsRealized)
{
focusedElement = element;
}
break;
}
child = parent as IInputElement;
parent = child.VisualParent;
}
}
// If the focused element has changed,
// we need to unpin the old one and pin the new one.
if (_lastFocusedElement != focusedElement)
{
if (_lastFocusedElement != null)
{
UpdatePin(_lastFocusedElement, false /* addPin */);
}
if (focusedElement != null)
{
UpdatePin(focusedElement, true /* addPin */);
}
_lastFocusedElement = focusedElement;
}
}
private void OnFocusChanged(object sender, RoutedEventArgs e) => UpdateFocusedElement();
private void EnsureEventSubscriptions()
{
if (!_eventsSubscribed)
{
_owner.GotFocus += OnFocusChanged;
_owner.LostFocus += OnFocusChanged;
}
}
private void UpdateElementIndex(IControl element, VirtualizationInfo virtInfo, int index)
{
var oldIndex = virtInfo.Index;
if (oldIndex != index)
{
virtInfo.UpdateIndex(index);
_owner.OnElementIndexChanged(element, oldIndex, index);
}
}
private void InvalidateRealizedIndicesHeldByLayout()
{
_firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault;
_lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault;
}
private struct PinnedElementInfo
{
public PinnedElementInfo(IControl element)
{
PinnedElement = element;
VirtualizationInfo = ItemsRepeater.GetVirtualizationInfo(element);
}
public IControl PinnedElement { get; }
public VirtualizationInfo VirtualizationInfo { get; }
}
}
}

501
src/Avalonia.Controls/Repeater/ViewportManager.cs

@ -0,0 +1,501 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Layout;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
internal class ViewportManager
{
private const double CacheBufferPerSideInflationPixelDelta = 40.0;
private readonly ItemsRepeater _owner;
private bool _ensuredScroller;
private IScrollAnchorProvider _scroller;
private IControl _makeAnchorElement;
private bool _isAnchorOutsideRealizedRange;
private Task _cacheBuildAction;
private Rect _visibleWindow;
private Rect _layoutExtent;
// This is the expected shift by the layout.
private Point _expectedViewportShift;
// This is what is pending and not been accounted for.
// Sometimes the scrolling surface cannot service a shift (for example
// it is already at the top and cannot shift anymore.)
private Point _pendingViewportShift;
// Unshiftable shift amount that this view manager can
// handle on its own to fake it to the layout as if the shift
// actually happened. This can happen in cases where no scrollviewer
// in the parent chain can scroll in the shift direction.
private Point _unshiftableShift;
private double _maximumHorizontalCacheLength = 0.0;
private double _maximumVerticalCacheLength = 0.0;
private double _horizontalCacheBufferPerSide;
private double _verticalCacheBufferPerSide;
private bool _isBringIntoViewInProgress;
// For non-virtualizing layouts, we do not need to keep
// updating viewports and invalidating measure often. So when
// a non virtualizing layout is used, we stop doing all that work.
bool _managingViewportDisabled;
private IDisposable _effectiveViewportChangedRevoker;
private bool _layoutUpdatedSubscribed;
public ViewportManager(ItemsRepeater owner)
{
_owner = owner;
}
public IControl SuggestedAnchor
{
get
{
// The element generated during the ItemsRepeater.MakeAnchor call has precedence over the next tick.
var suggestedAnchor = _makeAnchorElement;
var owner = _owner;
if (suggestedAnchor == null)
{
var anchorElement = _scroller?.CurrentAnchor;
if (anchorElement != null)
{
// We can't simply return anchorElement because, in case of nested Repeaters, it may not
// be a direct child of ours, or even an indirect child. We need to walk up the tree starting
// from anchorElement to figure out what child of ours (if any) to use as the suggested element.
var child = anchorElement;
var parent = child.VisualParent as IControl;
while (parent != null)
{
if (parent == owner)
{
suggestedAnchor = child;
break;
}
child = parent;
parent = parent.VisualParent as IControl;
}
}
}
return suggestedAnchor;
}
}
public bool HasScroller => _scroller != null;
public IControl MadeAnchor => _makeAnchorElement;
public double HorizontalCacheLength
{
get => _maximumHorizontalCacheLength;
set
{
if (_maximumHorizontalCacheLength != value)
{
ValidateCacheLength(value);
_maximumHorizontalCacheLength = value;
}
}
}
public double VerticalCacheLength
{
get => _maximumVerticalCacheLength;
set
{
if (_maximumVerticalCacheLength != value)
{
ValidateCacheLength(value);
_maximumVerticalCacheLength = value;
}
}
}
private Rect GetLayoutVisibleWindowDiscardAnchor()
{
var visibleWindow = _visibleWindow;
if (HasScroller)
{
visibleWindow = new Rect(
visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X,
visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y,
visibleWindow.Width,
visibleWindow.Height);
}
return visibleWindow;
}
public Rect GetLayoutVisibleWindow()
{
var visibleWindow = _visibleWindow;
if (_makeAnchorElement != null)
{
// The anchor is not necessarily laid out yet. Its position should default
// to zero and the layout origin is expected to change once layout is done.
// Until then, we need a window that's going to protect the anchor from
// getting recycled.
visibleWindow = visibleWindow.WithX(0).WithY(0);
}
else if (HasScroller)
{
visibleWindow = new Rect(
visibleWindow.X + _layoutExtent.X + _expectedViewportShift.X + _unshiftableShift.X,
visibleWindow.Y + _layoutExtent.Y + _expectedViewportShift.Y + _unshiftableShift.Y,
visibleWindow.Width,
visibleWindow.Height);
}
return visibleWindow;
}
public Rect GetLayoutRealizationWindow()
{
var realizationWindow = GetLayoutVisibleWindow();
if (HasScroller)
{
realizationWindow = new Rect(
realizationWindow.X - _horizontalCacheBufferPerSide,
realizationWindow.Y - _verticalCacheBufferPerSide,
realizationWindow.Width + _horizontalCacheBufferPerSide * 2.0,
realizationWindow.Height + _verticalCacheBufferPerSide * 2.0);
}
return realizationWindow;
}
public void SetLayoutExtent(Rect extent)
{
_expectedViewportShift = new Point(
_expectedViewportShift.X + _layoutExtent.X - extent.X,
_expectedViewportShift.Y + _layoutExtent.Y - extent.Y);
// We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much.
if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1)
{
// There are cases where we might be expecting a shift but not get it. We will
// be waiting for the effective viewport event but if the scroll viewer is not able
// to perform the shift (perhaps because it cannot scroll in negative offset),
// then we will end up not realizing elements in the visible
// window. To avoid this, we register to layout updated for this layout pass. If we
// get an effective viewport, we know we have a new viewport and we unregister from
// layout updated. If we get the layout updated handler, then we know that the
// scroller was unable to perform the shift and we invalidate measure and unregister
// from the layout updated event.
if (!_layoutUpdatedSubscribed)
{
_owner.LayoutUpdated += OnLayoutUpdated;
_layoutUpdatedSubscribed = true;
}
}
_layoutExtent = extent;
_pendingViewportShift = _expectedViewportShift;
// We just finished a measure pass and have a new extent.
// Let's make sure the scrollers will run its arrange so that they track the anchor.
((IControl)_scroller)?.InvalidateArrange();
}
public Point GetOrigin() => _layoutExtent.TopLeft;
public void OnLayoutChanged(bool isVirtualizing)
{
_managingViewportDisabled = !isVirtualizing;
_layoutExtent = default;
_expectedViewportShift = default;
_pendingViewportShift = default;
_unshiftableShift = default;
_effectiveViewportChangedRevoker?.Dispose();
if (!_managingViewportDisabled)
{
_effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner);
}
}
public void OnElementPrepared(IControl element)
{
// If we have an anchor element, we do not want the
// scroll anchor provider to start anchoring some other element.
////element.CanBeScrollAnchor(true);
}
public void OnElementCleared(ILayoutable element)
{
////element.CanBeScrollAnchor(false);
}
public void OnOwnerMeasuring()
{
// This is because of a bug that causes effective viewport to not
// fire if you register during arrange.
// Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport
EnsureScroller();
}
public void OnOwnerArranged()
{
_expectedViewportShift = default;
if (!_managingViewportDisabled)
{
// This is because of a bug that causes effective viewport to not
// fire if you register during arrange.
// Bug 17411076: EffectiveViewport: registering for effective viewport in arrange should invalidate viewport
// EnsureScroller();
if (HasScroller)
{
double maximumHorizontalCacheBufferPerSide = _maximumHorizontalCacheLength * _visibleWindow.Width / 2.0;
double maximumVerticalCacheBufferPerSide = _maximumVerticalCacheLength * _visibleWindow.Height / 2.0;
bool continueBuildingCache =
_horizontalCacheBufferPerSide < maximumHorizontalCacheBufferPerSide ||
_verticalCacheBufferPerSide < maximumVerticalCacheBufferPerSide;
if (continueBuildingCache)
{
_horizontalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta;
_verticalCacheBufferPerSide += CacheBufferPerSideInflationPixelDelta;
_horizontalCacheBufferPerSide = Math.Min(_horizontalCacheBufferPerSide, maximumHorizontalCacheBufferPerSide);
_verticalCacheBufferPerSide = Math.Min(_verticalCacheBufferPerSide, maximumVerticalCacheBufferPerSide);
}
}
}
}
private void OnLayoutUpdated(object sender, EventArgs args)
{
_owner.LayoutUpdated -= OnLayoutUpdated;
if (_managingViewportDisabled)
{
return;
}
// We were expecting a viewport shift but we never got one and we are not going to in this
// layout pass. We likely will never get this shift, so lets assume that we are never going to get it and
// adjust our expected shift to track that. One case where this can happen is when there is no scrollviewer
// that can scroll in the direction where the shift is expected.
if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0)
{
// Assume this is never going to come.
_unshiftableShift = new Point(
_unshiftableShift.X + _pendingViewportShift.X,
_unshiftableShift.Y + _pendingViewportShift.Y);
_pendingViewportShift = default;
_expectedViewportShift = default;
TryInvalidateMeasure();
}
}
public void OnMakeAnchor(IControl anchor, bool isAnchorOutsideRealizedRange)
{
_makeAnchorElement = anchor;
_isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange;
}
public void OnBringIntoViewRequested(RequestBringIntoViewEventArgs args)
{
if (!_managingViewportDisabled)
{
// During the time between a bring into view request and the element coming into view we do not
// want the anchor provider to pick some anchor and jump to it. Instead we want to anchor on the
// element that is being brought into view. We can do this by making just that element as a potential
// anchor candidate and ensure no other element of this repeater is an anchor candidate.
// Once the layout pass is done and we render the frame, the element will be in frame and we can
// switch back to letting the anchor provider pick a suitable anchor.
// get the targetChild - i.e the immediate child of this repeater that is being brought into view.
// Note that the element being brought into view could be a descendant.
var targetChild = GetImmediateChildOfRepeater((IControl)args.TargetObject);
// Make sure that only the target child can be the anchor during the bring into view operation.
foreach (var child in _owner.Children)
{
////if (child.CanBeScrollAnchor && child != targetChild)
////{
//// child.CanBeScrollAnchor = false;
////}
}
// Register to rendering event to go back to how things were before where any child can be the anchor.
_isBringIntoViewInProgress = true;
////if (!m_renderingToken)
////{
//// winrt::Windows::UI::Xaml::Media::CompositionTarget compositionTarget{ nullptr };
//// m_renderingToken = compositionTarget.Rendering(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering });
////}
}
}
private IControl GetImmediateChildOfRepeater(IControl descendant)
{
var targetChild = descendant;
var parent = descendant.Parent;
while (parent != null && parent != _owner)
{
targetChild = parent;
parent = (IControl)parent.VisualParent;
}
if (parent == null)
{
throw new InvalidOperationException("OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call");
}
return targetChild;
}
public void ResetScrollers()
{
_scroller = null;
_effectiveViewportChangedRevoker?.Dispose();
_effectiveViewportChangedRevoker = null;
_ensuredScroller = false;
}
private void OnEffectiveViewportChanged(TransformedBounds? bounds)
{
if (!bounds.HasValue)
{
return;
}
var globalClip = bounds.Value.Clip;
var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value;
var clip = globalClip.TransformToAABB(transform);
var effectiveViewport = clip.Intersect(bounds.Value.Bounds);
UpdateViewport(effectiveViewport);
_pendingViewportShift = default;
_unshiftableShift = default;
if (_visibleWindow.IsEmpty)
{
// We got cleared.
_layoutExtent = default;
}
// We got a new viewport, we dont need to wait for layout updated anymore to
// see if our request for a pending shift was handled.
if (_layoutUpdatedSubscribed)
{
_owner.LayoutUpdated -= OnLayoutUpdated;
}
}
private void EnsureScroller()
{
if (!_ensuredScroller)
{
ResetScrollers();
var parent = _owner.GetVisualParent();
while (parent != null)
{
if (parent is IScrollAnchorProvider scroller)
{
_scroller = scroller;
break;
}
parent = parent.VisualParent;
}
if (_scroller == null)
{
// We usually update the viewport in the post arrange handler. But, since we don't have
// a scroller, let's do it now.
UpdateViewport(Rect.Empty);
}
else if (!_managingViewportDisabled)
{
_effectiveViewportChangedRevoker?.Dispose();
_effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner);
}
_ensuredScroller = true;
}
}
private void UpdateViewport(Rect viewport)
{
var currentVisibleWindow = viewport;
if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X &&
-currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y)
{
// We got cleared.
_visibleWindow = default;
}
else
{
_visibleWindow = currentVisibleWindow;
}
TryInvalidateMeasure();
}
private static void ValidateCacheLength(double cacheLength)
{
if (cacheLength < 0.0 || double.IsInfinity(cacheLength) || double.IsNaN(cacheLength))
{
throw new ArgumentException("The maximum cache length must be equal or superior to zero.");
}
}
private void TryInvalidateMeasure()
{
// Don't invalidate measure if we have an invalid window.
if (!_visibleWindow.IsEmpty)
{
// We invalidate measure instead of just invalidating arrange because
// we don't invalidate measure in UpdateViewport if the view is changing to
// avoid layout cycles.
_owner.InvalidateMeasure();
}
}
private IDisposable SubscribeToEffectiveViewportChanged(IControl control)
{
// HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater -
// we can get this from TransformedBounds, but this property is updated after layout has
// run, resulting in the UI being updated too late when scrolling quickly. We can
// partially remedey this by triggering also on Bounds changes, but this won't work so
// well for nested ItemsRepeaters.
//
// UWP uses the EffectiveBoundsChanged event (which I think was implemented specially
// for this case): we need to implement that in Avalonia.
return control.GetObservable(Visual.TransformedBoundsProperty)
.Merge(control.GetObservable(Visual.BoundsProperty).Select(_ => control.TransformedBounds))
.Skip(1)
.Subscribe(OnEffectiveViewportChanged);
}
private class ScrollerInfo
{
public ScrollerInfo(ScrollViewer scroller)
{
Scroller = scroller;
}
public ScrollViewer Scroller { get; }
}
};
}

118
src/Avalonia.Controls/Repeater/VirtualizationInfo.cs

@ -0,0 +1,118 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
namespace Avalonia.Controls
{
internal enum ElementOwner
{
// All elements are originally owned by the view generator.
ElementFactory,
// Ownership is transferred to the layout when it calls GetElement.
Layout,
// Ownership is transferred to the pinned pool if the element is cleared (outside of
// a 'remove' collection change of course).
PinnedPool,
// Ownership is transfered to the reset pool if the element is cleared by a reset and
// the data source supports unique ids.
UniqueIdResetPool,
// Ownership is transfered to the animator if the element is cleared due to a
// 'remove'-like collection change.
Animator
}
internal class VirtualizationInfo
{
private int _pinCounter;
private object _data;
public Rect ArrangeBounds { get; set; }
public bool AutoRecycleCandidate { get; set; }
public int Index { get; private set; }
public bool IsPinned => _pinCounter > 0;
public bool IsHeldByLayout => Owner == ElementOwner.Layout;
public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool;
public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool;
public bool KeepAlive { get; set; }
public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory;
public string UniqueId { get; private set; }
public void MoveOwnershipToLayoutFromElementFactory(int index, string uniqueId)
{
Owner = ElementOwner.Layout;
Index = index;
UniqueId = uniqueId;
}
public void MoveOwnershipToLayoutFromUniqueIdResetPool()
{
Owner = ElementOwner.Layout;
}
public void MoveOwnershipToLayoutFromPinnedPool()
{
Owner = ElementOwner.Layout;
}
public void MoveOwnershipToElementFactory()
{
Owner = ElementOwner.ElementFactory;
_pinCounter = 0;
Index = -1;
UniqueId = string.Empty;
ArrangeBounds = ItemsRepeater.InvalidRect;
}
public void MoveOwnershipToUniqueIdResetPoolFromLayout()
{
Owner = ElementOwner.UniqueIdResetPool;
// Keep the pinCounter the same. If the container survives the reset
// it can go on being pinned as if nothing happened.
}
public void MoveOwnershipToAnimator()
{
// During a unique id reset, some elements might get removed.
// Their ownership will go from the UniqueIdResetPool to the Animator.
// The common path though is for ownership to go from Layout to Animator.
Owner = ElementOwner.Animator;
Index = -1;
_pinCounter = 0;
}
public void MoveOwnershipToPinnedPool()
{
Owner = ElementOwner.PinnedPool;
}
public int AddPin()
{
if (!IsRealized)
{
throw new InvalidOperationException("You can't pin an unrealized element.");
}
return ++_pinCounter;
}
public int RemovePin()
{
if (!IsRealized)
{
throw new InvalidOperationException("You can't unpin an unrealized element.");
}
if (!IsPinned)
{
throw new InvalidOperationException("UnpinElement was called more often than PinElement.");
}
return --_pinCounter;
}
public void UpdateIndex(int newIndex) => Index = newIndex;
}
}

15
src/Avalonia.Controls/ScrollViewer.cs

@ -11,7 +11,7 @@ namespace Avalonia.Controls
/// <summary>
/// A control scrolls its content if the content is bigger than the space available.
/// </summary>
public class ScrollViewer : ContentControl, IScrollable
public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider
{
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
@ -333,6 +333,9 @@ namespace Avalonia.Controls
get { return _viewport.Height; }
}
/// <inheritdoc/>
IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement
/// <summary>
/// Gets the value of the HorizontalScrollBarVisibility attached property.
/// </summary>
@ -373,6 +376,16 @@ namespace Avalonia.Controls
control.SetValue(VerticalScrollBarVisibilityProperty, value);
}
void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element)
{
// TODO: Implement
}
void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element)
{
// TODO: Implement
}
internal static Vector CoerceOffset(Size extent, Size viewport, Vector offset)
{
var maxX = Math.Max(extent.Width - viewport.Width, 0);

1
src/Avalonia.Controls/Slider.cs

@ -5,6 +5,7 @@ using System;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace Avalonia.Controls
{

149
src/Avalonia.Controls/StackPanel.cs

@ -1,8 +1,9 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
// This source file is adapted from the Windows Presentation Foundation project.
// (https://github.com/dotnet/wpf/)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Linq;
using Avalonia.Input;
using Avalonia.Layout;
@ -17,13 +18,13 @@ namespace Avalonia.Controls
/// Defines the <see cref="Spacing"/> property.
/// </summary>
public static readonly StyledProperty<double> SpacingProperty =
AvaloniaProperty.Register<StackPanel, double>(nameof(Spacing));
StackLayout.SpacingProperty.AddOwner<StackPanel>();
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<StackPanel, Orientation>(nameof(Orientation), Orientation.Vertical);
StackLayout.OrientationProperty.AddOwner<StackPanel>();
/// <summary>
/// Initializes static members of the <see cref="StackPanel"/> class.
@ -155,106 +156,122 @@ namespace Avalonia.Controls
}
/// <summary>
/// Measures the control.
/// General StackPanel layout behavior is to grow unbounded in the "stacking" direction (Size To Content).
/// Children in this dimension are encouraged to be as large as they like. In the other dimension,
/// StackPanel will assume the maximum size of its children.
/// </summary>
/// <param name="availableSize">The available size.</param>
/// <returns>The desired size of the control.</returns>
/// <param name="availableSize">Constraint</param>
/// <returns>Desired size</returns>
protected override Size MeasureOverride(Size availableSize)
{
double childAvailableWidth = double.PositiveInfinity;
double childAvailableHeight = double.PositiveInfinity;
Size stackDesiredSize = new Size();
var children = Children;
Size layoutSlotSize = availableSize;
bool fHorizontal = (Orientation == Orientation.Horizontal);
double spacing = Spacing;
bool hasVisibleChild = false;
if (Orientation == Orientation.Vertical)
//
// Initialize child sizing and iterator data
// Allow children as much size as they want along the stack.
//
if (fHorizontal)
{
childAvailableWidth = availableSize.Width;
if (!double.IsNaN(Width))
{
childAvailableWidth = Width;
}
childAvailableWidth = Math.Min(childAvailableWidth, MaxWidth);
childAvailableWidth = Math.Max(childAvailableWidth, MinWidth);
layoutSlotSize = layoutSlotSize.WithWidth(Double.PositiveInfinity);
}
else
{
childAvailableHeight = availableSize.Height;
layoutSlotSize = layoutSlotSize.WithHeight(Double.PositiveInfinity);
}
if (!double.IsNaN(Height))
{
childAvailableHeight = Height;
}
//
// Iterate through children.
// While we still supported virtualization, this was hidden in a child iterator (see source history).
//
for (int i = 0, count = children.Count; i < count; ++i)
{
// Get next child.
var child = children[i];
childAvailableHeight = Math.Min(childAvailableHeight, MaxHeight);
childAvailableHeight = Math.Max(childAvailableHeight, MinHeight);
}
if (child == null)
{ continue; }
double measuredWidth = 0;
double measuredHeight = 0;
double spacing = Spacing;
bool hasVisibleChild = Children.Any(c => c.IsVisible);
bool isVisible = child.IsVisible;
foreach (Control child in Children)
{
child.Measure(new Size(childAvailableWidth, childAvailableHeight));
Size size = child.DesiredSize;
if (isVisible && !hasVisibleChild)
{
hasVisibleChild = true;
}
if (Orientation == Orientation.Vertical)
// Measure the child.
child.Measure(layoutSlotSize);
Size childDesiredSize = child.DesiredSize;
// Accumulate child size.
if (fHorizontal)
{
measuredHeight += size.Height + (child.IsVisible ? spacing : 0);
measuredWidth = Math.Max(measuredWidth, size.Width);
stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width + (isVisible ? spacing : 0) + childDesiredSize.Width);
stackDesiredSize = stackDesiredSize.WithHeight(Math.Max(stackDesiredSize.Height, childDesiredSize.Height));
}
else
{
measuredWidth += size.Width + (child.IsVisible ? spacing : 0);
measuredHeight = Math.Max(measuredHeight, size.Height);
stackDesiredSize = stackDesiredSize.WithWidth(Math.Max(stackDesiredSize.Width, childDesiredSize.Width));
stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height + (isVisible ? spacing : 0) + childDesiredSize.Height);
}
}
if (Orientation == Orientation.Vertical)
if (fHorizontal)
{
measuredHeight -= (hasVisibleChild ? spacing : 0);
stackDesiredSize = stackDesiredSize.WithWidth(stackDesiredSize.Width - (hasVisibleChild ? spacing : 0));
}
else
{
measuredWidth -= (hasVisibleChild ? spacing : 0);
{
stackDesiredSize = stackDesiredSize.WithHeight(stackDesiredSize.Height - (hasVisibleChild ? spacing : 0));
}
return new Size(measuredWidth, measuredHeight).Constrain(availableSize);
return stackDesiredSize;
}
/// <inheritdoc/>
/// <summary>
/// Content arrangement.
/// </summary>
/// <param name="finalSize">Arrange size</param>
protected override Size ArrangeOverride(Size finalSize)
{
var orientation = Orientation;
var children = Children;
bool fHorizontal = (Orientation == Orientation.Horizontal);
Rect rcChild = new Rect(finalSize);
double previousChildSize = 0.0;
var spacing = Spacing;
var finalRect = new Rect(finalSize);
var pos = 0.0;
foreach (Control child in Children)
//
// Arrange and Position Children.
//
for (int i = 0, count = children.Count; i < count; ++i)
{
if (!child.IsVisible)
{
continue;
}
var child = children[i];
double childWidth = child.DesiredSize.Width;
double childHeight = child.DesiredSize.Height;
if (child == null)
{ continue; }
if (orientation == Orientation.Vertical)
if (fHorizontal)
{
var rect = new Rect(0, pos, childWidth, childHeight)
.Align(finalRect, child.HorizontalAlignment, VerticalAlignment.Top);
ArrangeChild(child, rect, finalSize, orientation);
pos += childHeight + spacing;
rcChild = rcChild.WithX(rcChild.X + previousChildSize);
previousChildSize = child.DesiredSize.Width;
rcChild = rcChild.WithWidth(previousChildSize);
rcChild = rcChild.WithHeight(Math.Max(finalSize.Height, child.DesiredSize.Height));
previousChildSize += spacing;
}
else
{
var rect = new Rect(pos, 0, childWidth, childHeight)
.Align(finalRect, HorizontalAlignment.Left, child.VerticalAlignment);
ArrangeChild(child, rect, finalSize, orientation);
pos += childWidth + spacing;
rcChild = rcChild.WithY(rcChild.Y + previousChildSize);
previousChildSize = child.DesiredSize.Height;
rcChild = rcChild.WithHeight(previousChildSize);
rcChild = rcChild.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width));
previousChildSize += spacing;
}
ArrangeChild(child, rcChild, finalSize, Orientation);
}
return finalSize;

35
src/Avalonia.Controls/Templates/FuncMemberSelector.cs

@ -1,35 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Controls.Templates
{
/// <summary>
/// Selects a member of an object using a <see cref="Func{TObject, TMember}"/>.
/// </summary>
public class FuncMemberSelector<TObject, TMember> : IMemberSelector
{
private readonly Func<TObject, TMember> _selector;
/// <summary>
/// Initializes a new instance of the <see cref="FuncMemberSelector{TObject, TMember}"/>
/// class.
/// </summary>
/// <param name="selector">The selector.</param>
public FuncMemberSelector(Func<TObject, TMember> selector)
{
this._selector = selector;
}
/// <summary>
/// Selects a member of an object.
/// </summary>
/// <param name="o">The object.</param>
/// <returns>The selected member.</returns>
public object Select(object o)
{
return (o is TObject) ? _selector((TObject)o) : default(TMember);
}
}
}

18
src/Avalonia.Controls/Templates/IMemberSelector.cs

@ -1,18 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Controls.Templates
{
/// <summary>
/// Selects a member of an object.
/// </summary>
public interface IMemberSelector
{
/// <summary>
/// Selects a member of an object.
/// </summary>
/// <param name="o">The object.</param>
/// <returns>The selected member.</returns>
object Select(object o);
}
}

29
src/Avalonia.Controls/TextBlock.cs

@ -1,12 +1,9 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive;
using System.Reactive.Linq;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Metadata;
namespace Avalonia.Controls
@ -106,6 +103,14 @@ namespace Avalonia.Controls
FontWeightProperty,
FontSizeProperty,
FontStyleProperty);
Observable.Merge(
TextProperty.Changed,
TextAlignmentProperty.Changed,
FontSizeProperty.Changed,
FontStyleProperty.Changed,
FontWeightProperty.Changed
).AddClassHandler<TextBlock>((x,_) => x.OnTextPropertiesChanged());
}
/// <summary>
@ -114,18 +119,6 @@ namespace Avalonia.Controls
public TextBlock()
{
_text = string.Empty;
Observable.Merge(
this.GetObservable(TextProperty).Select(_ => Unit.Default),
this.GetObservable(TextAlignmentProperty).Select(_ => Unit.Default),
this.GetObservable(FontSizeProperty).Select(_ => Unit.Default),
this.GetObservable(FontStyleProperty).Select(_ => Unit.Default),
this.GetObservable(FontWeightProperty).Select(_ => Unit.Default))
.Subscribe(_ =>
{
InvalidateFormattedText();
InvalidateMeasure();
});
}
/// <summary>
@ -408,5 +401,11 @@ namespace Avalonia.Controls
InvalidateFormattedText();
InvalidateMeasure();
}
private void OnTextPropertiesChanged()
{
InvalidateFormattedText();
InvalidateMeasure();
}
}
}

105
src/Avalonia.Controls/TreeView.cs

@ -105,32 +105,21 @@ namespace Avalonia.Controls
get => _selectedItem;
set
{
SetAndRaise(SelectedItemProperty, ref _selectedItem,
(object val, ref object backing, Action<Action> notifyWrapper) =>
{
var old = backing;
backing = val;
notifyWrapper(() =>
RaisePropertyChanged(
SelectedItemProperty,
old,
val));
SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
if (val != null)
{
if (SelectedItems.Count != 1 || SelectedItems[0] != val)
{
_syncingSelectedItems = true;
SelectSingleItem(val);
_syncingSelectedItems = false;
}
}
else if (SelectedItems.Count > 0)
{
SelectedItems.Clear();
}
}, value);
if (value != null)
{
if (SelectedItems.Count != 1 || SelectedItems[0] != value)
{
_syncingSelectedItems = true;
SelectSingleItem(value);
_syncingSelectedItems = false;
}
}
else if (SelectedItems.Count > 0)
{
SelectedItems.Clear();
}
}
}
@ -164,6 +153,48 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Expands the specified <see cref="TreeViewItem"/> all descendent <see cref="TreeViewItem"/>s.
/// </summary>
/// <param name="item">The item to expand.</param>
public void ExpandSubTree(TreeViewItem item)
{
item.IsExpanded = true;
var panel = item.Presenter.Panel;
if (panel != null)
{
foreach (var child in panel.Children)
{
if (child is TreeViewItem treeViewItem)
{
ExpandSubTree(treeViewItem);
}
}
}
}
/// <summary>
/// Selects all items in the <see cref="TreeView"/>.
/// </summary>
/// <remarks>
/// Note that this method only selects nodes currently visible due to their parent nodes
/// being expanded: it does not expand nodes.
/// </remarks>
public void SelectAll()
{
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
}
/// <summary>
/// Deselects all items in the <see cref="TreeView"/>.
/// </summary>
public void UnselectAll()
{
SelectedItems.Clear();
}
/// <summary>
/// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary>
@ -409,7 +440,7 @@ namespace Avalonia.Controls
if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll))
{
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
SelectAll();
e.Handled = true;
}
}
@ -479,7 +510,8 @@ namespace Avalonia.Controls
e.Source,
true,
(e.InputModifiers & InputModifiers.Shift) != 0,
(e.InputModifiers & InputModifiers.Control) != 0);
(e.InputModifiers & InputModifiers.Control) != 0,
e.MouseButton == MouseButton.Right);
}
}
@ -490,11 +522,13 @@ namespace Avalonia.Controls
/// <param name="select">Whether the item should be selected or unselected.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
protected void UpdateSelectionFromContainer(
IControl container,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
bool toggleModifier = false,
bool rightButton = false)
{
var item = ItemContainerGenerator.Index.ItemFromContainer(container);
@ -515,7 +549,14 @@ namespace Avalonia.Controls
var multi = (mode & SelectionMode.Multiple) != 0;
var range = multi && selectedContainer != null && rangeModifier;
if (!toggle && !range)
if (rightButton)
{
if (!SelectedItems.Contains(item))
{
SelectSingleItem(item);
}
}
else if (!toggle && !range)
{
SelectSingleItem(item);
}
@ -684,6 +725,7 @@ namespace Avalonia.Controls
/// <param name="select">Whether the container should be selected or unselected.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
/// <returns>
/// True if the event originated from a container that belongs to the control; otherwise
/// false.
@ -692,13 +734,14 @@ namespace Avalonia.Controls
IInteractive eventSource,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
bool toggleModifier = false,
bool rightButton = false)
{
var container = GetContainerFromEventSource(eventSource);
if (container != null)
{
UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier);
UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier, rightButton);
return true;
}

1
src/Avalonia.Controls/WrapPanel.cs

@ -4,6 +4,7 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Utilities;
using static System.Math;

2
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@ -54,7 +54,7 @@ namespace Avalonia.DesignerSupport.Remote
.Bind<IPlatformSettings>().ToConstant(instance)
.Bind<IPlatformThreadingInterface>().ToConstant(threading)
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(threading)
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogsStub>()
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()

27
src/Avalonia.Diagnostics/DevTools.xaml

@ -1,23 +1,24 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia.Diagnostics.DevTools">
<Grid RowDefinitions="Auto,*,Auto">
<TabStrip SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
<TabStripItem Content="Logical Tree"/>
<TabStripItem Content="Visual Tree"/>
<TabStripItem Content="Events"/>
</TabStrip>
<Grid RowDefinitions="*,Auto" Margin="4">
<ContentControl Content="{Binding Content}" Grid.Row="1"/>
<StackPanel Spacing="4" Orientation="Horizontal" Grid.Row="2">
<TabControl Grid.Row="0" Items="{Binding Tools}" SelectedItem="{Binding SelectedTool}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
<StackPanel Grid.Row="1" Spacing="4" Orientation="Horizontal">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8"/>
<Separator Width="8" />
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}"/>
<Separator Width="8"/>
<TextBlock Text="{Binding FocusedControl}" />
<Separator Width="8" />
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}"/>
<TextBlock Text="{Binding PointerOverElement}" />
</StackPanel>
</Grid>
</UserControl>

37
src/Avalonia.Diagnostics/DevTools.xaml.cs

@ -1,10 +1,12 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Input.Raw;
@ -15,22 +17,22 @@ using Avalonia.VisualTree;
namespace Avalonia
{
public static class DevToolsExtensions
{
public static void AttachDevTools(this TopLevel control)
{
Avalonia.Diagnostics.DevTools.Attach(control);
}
}
public static class DevToolsExtensions
{
public static void AttachDevTools(this TopLevel control)
{
Diagnostics.DevTools.Attach(control);
}
}
}
namespace Avalonia.Diagnostics
{
public class DevTools : UserControl
public class DevTools : UserControl
{
private static Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
private static HashSet<IRenderRoot> s_visualTreeRoots = new HashSet<IRenderRoot>();
private IDisposable _keySubscription;
private static readonly Dictionary<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
private static readonly HashSet<IRenderRoot> s_visualTreeRoots = new HashSet<IRenderRoot>();
private readonly IDisposable _keySubscription;
public DevTools(IControl root)
{
@ -46,7 +48,6 @@ namespace Avalonia.Diagnostics
// HACK: needed for XAMLIL, will fix that later
public DevTools()
{
}
public IControl Root { get; }
@ -64,9 +65,8 @@ namespace Avalonia.Diagnostics
if (e.Key == Key.F12)
{
var control = (TopLevel)sender;
var devToolsWindow = default(Window);
if (s_open.TryGetValue(control, out devToolsWindow))
if (s_open.TryGetValue(control, out var devToolsWindow))
{
devToolsWindow.Activate();
}
@ -79,10 +79,8 @@ namespace Avalonia.Diagnostics
Width = 1024,
Height = 512,
Content = devTools,
DataTemplates =
{
new ViewLocator<ViewModelBase>(),
}
DataTemplates = { new ViewLocator<ViewModelBase>() },
Title = "Avalonia DevTools"
};
devToolsWindow.Closed += devTools.DevToolsClosed;
@ -114,7 +112,6 @@ namespace Avalonia.Diagnostics
if ((e.Modifiers) == modifiers)
{
var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point);
var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible))
.FirstOrDefault();

7
src/Avalonia.Diagnostics/Models/EventChainLink.cs

@ -12,9 +12,9 @@ namespace Avalonia.Diagnostics.Models
{
Contract.Requires<ArgumentNullException>(handler != null);
this.Handler = handler;
this.Handled = handled;
this.Route = route;
Handler = handler;
Handled = handled;
Route = route;
}
public object Handler { get; }
@ -27,6 +27,7 @@ namespace Avalonia.Diagnostics.Models
{
return named.Name + " (" + Handler.GetType().Name + ")";
}
return Handler.GetType().Name;
}
}

4
src/Avalonia.Diagnostics/ViewLocator.cs

@ -7,7 +7,7 @@ using Avalonia.Controls.Templates;
namespace Avalonia.Diagnostics
{
public class ViewLocator<TViewModel> : IDataTemplate
internal class ViewLocator<TViewModel> : IDataTemplate
{
public bool SupportsRecycling => false;
@ -31,4 +31,4 @@ namespace Avalonia.Diagnostics
return data is TViewModel;
}
}
}
}

12
src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs

@ -20,16 +20,6 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public IEnumerable<string> Classes
{
get;
private set;
}
public IEnumerable<PropertyDetails> Properties
{
get;
private set;
}
public IEnumerable<PropertyDetails> Properties { get; }
}
}

64
src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs

@ -2,7 +2,8 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Linq;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Input;
@ -10,21 +11,23 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class DevToolsViewModel : ViewModelBase
{
private ViewModelBase _content;
private int _selectedTab;
private TreePageViewModel _logicalTree;
private TreePageViewModel _visualTree;
private EventsViewModel _eventsView;
private IDevToolViewModel _selectedTool;
private string _focusedControl;
private string _pointerOverElement;
public DevToolsViewModel(IControl root)
{
_logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
_eventsView = new EventsViewModel(root);
Tools = new ObservableCollection<IDevToolViewModel>
{
new TreePageViewModel(LogicalTreeNode.Create(root), "Logical Tree"),
new TreePageViewModel(VisualTreeNode.Create(root), "Visual Tree"),
new EventsViewModel(root)
};
SelectedTool = Tools.First();
UpdateFocusedControl();
KeyboardDevice.Instance.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
@ -33,58 +36,33 @@ namespace Avalonia.Diagnostics.ViewModels
}
};
SelectedTab = 0;
root.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x?.GetType().Name);
}
public ViewModelBase Content
public IDevToolViewModel SelectedTool
{
get { return _content; }
private set { RaiseAndSetIfChanged(ref _content, value); }
get => _selectedTool;
set => RaiseAndSetIfChanged(ref _selectedTool, value);
}
public int SelectedTab
{
get { return _selectedTab; }
set
{
_selectedTab = value;
switch (value)
{
case 0:
Content = _logicalTree;
break;
case 1:
Content = _visualTree;
break;
case 2:
Content = _eventsView;
break;
}
RaisePropertyChanged();
}
}
public ObservableCollection<IDevToolViewModel> Tools { get; }
public string FocusedControl
{
get { return _focusedControl; }
private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
get => _focusedControl;
private set => RaiseAndSetIfChanged(ref _focusedControl, value);
}
public string PointerOverElement
{
get { return _pointerOverElement; }
private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); }
get => _pointerOverElement;
private set => RaiseAndSetIfChanged(ref _pointerOverElement, value);
}
public void SelectControl(IControl control)
{
var tree = Content as TreePageViewModel;
if (tree != null)
if (SelectedTool is TreePageViewModel tree)
{
tree.SelectControl(control);
}

15
src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs

@ -13,22 +13,18 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class EventOwnerTreeNode : EventTreeNodeBase
{
private static readonly RoutedEvent[] s_defaultEvents = new RoutedEvent[]
private static readonly RoutedEvent[] s_defaultEvents =
{
Button.ClickEvent,
InputElement.KeyDownEvent,
InputElement.KeyUpEvent,
InputElement.TextInputEvent,
InputElement.PointerReleasedEvent,
InputElement.PointerPressedEvent,
Button.ClickEvent, InputElement.KeyDownEvent, InputElement.KeyUpEvent, InputElement.TextInputEvent,
InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent
};
public EventOwnerTreeNode(Type type, IEnumerable<RoutedEvent> events, EventsViewModel vm)
: base(null, type.Name)
{
this.Children = new AvaloniaList<EventTreeNodeBase>(events.OrderBy(e => e.Name)
Children = new AvaloniaList<EventTreeNodeBase>(events.OrderBy(e => e.Name)
.Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) }));
this.IsExpanded = true;
IsExpanded = true;
}
public override bool? IsEnabled
@ -39,6 +35,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (base.IsEnabled != value)
{
base.IsEnabled = value;
if (_updateChildren && value != null)
{
foreach (var child in Children)

9
src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Diagnostics.Models;
using Avalonia.Interactivity;
using Avalonia.Threading;
@ -12,8 +11,8 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class EventTreeNode : EventTreeNodeBase
{
private RoutedEvent _event;
private EventsViewModel _parentViewModel;
private readonly RoutedEvent _event;
private readonly EventsViewModel _parentViewModel;
private bool _isRegistered;
private FiredEvent _currentEvent;
@ -23,8 +22,8 @@ namespace Avalonia.Diagnostics.ViewModels
Contract.Requires<ArgumentNullException>(@event != null);
Contract.Requires<ArgumentNullException>(vm != null);
this._event = @event;
this._parentViewModel = vm;
_event = @event;
_parentViewModel = vm;
}
public override bool? IsEnabled

17
src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs

@ -12,10 +12,10 @@ namespace Avalonia.Diagnostics.ViewModels
private bool _isExpanded;
private bool? _isEnabled = false;
public EventTreeNodeBase(EventTreeNodeBase parent, string text)
protected EventTreeNodeBase(EventTreeNodeBase parent, string text)
{
this.Parent = parent;
this.Text = text;
Parent = parent;
Text = text;
}
public IAvaloniaReadOnlyList<EventTreeNodeBase> Children
@ -26,14 +26,14 @@ namespace Avalonia.Diagnostics.ViewModels
public bool IsExpanded
{
get { return _isExpanded; }
set { RaiseAndSetIfChanged(ref _isExpanded, value); }
get => _isExpanded;
set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
public virtual bool? IsEnabled
{
get { return _isEnabled; }
set { RaiseAndSetIfChanged(ref _isEnabled, value); }
get => _isEnabled;
set => RaiseAndSetIfChanged(ref _isEnabled, value);
}
public EventTreeNodeBase Parent
@ -44,7 +44,6 @@ namespace Avalonia.Diagnostics.ViewModels
public string Text
{
get;
private set;
}
internal void UpdateChecked()
@ -55,7 +54,9 @@ namespace Avalonia.Diagnostics.ViewModels
{
if (Children == null)
return false;
bool? value = false;
for (int i = 0; i < Children.Count; i++)
{
if (i == 0)

13
src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs

@ -5,8 +5,6 @@ using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Windows.Input;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Interactivity;
@ -14,21 +12,24 @@ using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventsViewModel : ViewModelBase
internal class EventsViewModel : ViewModelBase, IDevToolViewModel
{
private readonly IControl _root;
private FiredEvent _selectedEvent;
public EventsViewModel(IControl root)
{
this._root = root;
this.Nodes = RoutedEventRegistry.Instance.GetAllRegistered()
_root = root;
Nodes = RoutedEventRegistry.Instance.GetAllRegistered()
.GroupBy(e => e.OwnerType)
.OrderBy(e => e.Key.Name)
.Select(g => new EventOwnerTreeNode(g.Key, g, this))
.ToArray();
}
public string Name => "Events";
public EventTreeNodeBase[] Nodes { get; }
public ObservableCollection<FiredEvent> RecordedEvents { get; } = new ObservableCollection<FiredEvent>();
@ -49,7 +50,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? Brushes.LightGreen : Brushes.Transparent;
return (bool)value ? Brushes.Green : Brushes.Transparent;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

12
src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs

@ -3,7 +3,6 @@
using System;
using System.Collections.ObjectModel;
using Avalonia.Diagnostics.Models;
using Avalonia.Interactivity;
@ -11,7 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class FiredEvent : ViewModelBase
{
private RoutedEventArgs _eventArgs;
private readonly RoutedEventArgs _eventArgs;
private EventChainLink _handledBy;
public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator)
@ -19,8 +18,8 @@ namespace Avalonia.Diagnostics.ViewModels
Contract.Requires<ArgumentNullException>(eventArgs != null);
Contract.Requires<ArgumentNullException>(originator != null);
this._eventArgs = eventArgs;
this.Originator = originator;
_eventArgs = eventArgs;
Originator = originator;
AddToChain(originator);
}
@ -42,8 +41,9 @@ namespace Avalonia.Diagnostics.ViewModels
if (IsHandled)
{
return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine +
$"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}";
$"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}";
}
return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}";
}
}
@ -52,7 +52,7 @@ namespace Avalonia.Diagnostics.ViewModels
public EventChainLink HandledBy
{
get { return _handledBy; }
get => _handledBy;
set
{
if (_handledBy != value)

16
src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs

@ -0,0 +1,16 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Diagnostics.ViewModels
{
/// <summary>
/// View model interface for tool showing up in DevTools
/// </summary>
public interface IDevToolViewModel
{
/// <summary>
/// Name of a tool.
/// </summary>
string Name { get; }
}
}

3
src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs

@ -17,8 +17,7 @@ namespace Avalonia.Diagnostics.ViewModels
public static LogicalTreeNode[] Create(object control)
{
var logical = control as ILogical;
return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null;
return control is ILogical logical ? new[] { new LogicalTreeNode(logical, null) } : null;
}
}
}

16
src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs

@ -26,7 +26,9 @@ namespace Avalonia.Diagnostics.ViewModels
Value = diagnostic.Value ?? "(null)";
Priority = (diagnostic.Priority != BindingPriority.Unset) ?
diagnostic.Priority.ToString() :
diagnostic.Property.Inherits ? "Inherited" : "Unset";
diagnostic.Property.Inherits ?
"Inherited" :
"Unset";
Diagnostic = diagnostic.Diagnostic;
});
}
@ -37,20 +39,20 @@ namespace Avalonia.Diagnostics.ViewModels
public string Priority
{
get { return _priority; }
private set { RaiseAndSetIfChanged(ref _priority, value); }
get => _priority;
private set => RaiseAndSetIfChanged(ref _priority, value);
}
public string Diagnostic
{
get { return _diagnostic; }
private set { RaiseAndSetIfChanged(ref _diagnostic, value); }
get => _diagnostic;
private set => RaiseAndSetIfChanged(ref _diagnostic, value);
}
public object Value
{
get { return _value; }
private set { RaiseAndSetIfChanged(ref _value, value); }
get => _value;
private set => RaiseAndSetIfChanged(ref _value, value);
}
}
}

15
src/Avalonia.Diagnostics/ViewModels/TreeNode.cs

@ -27,9 +27,9 @@ namespace Avalonia.Diagnostics.ViewModels
var classesChanged = Observable.FromEventPattern<
NotifyCollectionChangedEventHandler,
NotifyCollectionChangedEventArgs>(
x => styleable.Classes.CollectionChanged += x,
x => styleable.Classes.CollectionChanged -= x)
.TakeUntil(((IStyleable)styleable).StyleDetach);
x => styleable.Classes.CollectionChanged += x,
x => styleable.Classes.CollectionChanged -= x)
.TakeUntil(styleable.StyleDetach);
classesChanged.Select(_ => Unit.Default)
.StartWith(Unit.Default)
@ -55,8 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels
public string Classes
{
get { return _classes; }
private set { RaiseAndSetIfChanged(ref _classes, value); }
get => _classes;
private set => RaiseAndSetIfChanged(ref _classes, value);
}
public IVisual Visual
@ -66,8 +66,8 @@ namespace Avalonia.Diagnostics.ViewModels
public bool IsExpanded
{
get { return _isExpanded; }
set { RaiseAndSetIfChanged(ref _isExpanded, value); }
get => _isExpanded;
set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
public TreeNode Parent
@ -78,7 +78,6 @@ namespace Avalonia.Diagnostics.ViewModels
public string Type
{
get;
private set;
}
}
}

29
src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs

@ -6,21 +6,24 @@ using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
internal class TreePageViewModel : ViewModelBase
internal class TreePageViewModel : ViewModelBase, IDevToolViewModel
{
private TreeNode _selected;
private ControlDetailsViewModel _details;
public TreePageViewModel(TreeNode[] nodes)
public TreePageViewModel(TreeNode[] nodes, string name)
{
Nodes = nodes;
Name = name;
}
public string Name { get; }
public TreeNode[] Nodes { get; protected set; }
public TreeNode SelectedNode
{
get { return _selected; }
get => _selected;
set
{
if (RaiseAndSetIfChanged(ref _selected, value))
@ -32,8 +35,8 @@ namespace Avalonia.Diagnostics.ViewModels
public ControlDetailsViewModel Details
{
get { return _details; }
private set { RaiseAndSetIfChanged(ref _details, value); }
get => _details;
private set => RaiseAndSetIfChanged(ref _details, value);
}
public TreeNode FindNode(IControl control)
@ -63,7 +66,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
control = control.GetVisualParent<IControl>();
}
}
}
if (node != null)
{
@ -87,16 +90,14 @@ namespace Avalonia.Diagnostics.ViewModels
{
return node;
}
else
foreach (var child in node.Children)
{
foreach (var child in node.Children)
{
var result = FindNode(child, control);
var result = FindNode(child, control);
if (result != null)
{
return result;
}
if (result != null)
{
return result;
}
}

7
src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs

@ -1,11 +1,14 @@
using System.Collections.Generic;
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
namespace Avalonia.Diagnostics.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
internal class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;

5
src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs

@ -29,12 +29,11 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public bool IsInTemplate { get; private set; }
public bool IsInTemplate { get; }
public static VisualTreeNode[] Create(object control)
{
var visual = control as IVisual;
return visual != null ? new[] { new VisualTreeNode(visual, null) } : null;
return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : null;
}
}
}

33
src/Avalonia.Diagnostics/Views/ControlDetailsView.cs

@ -7,7 +7,6 @@ using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Media;
using Avalonia.Styling;
namespace Avalonia.Diagnostics.Views
{
@ -15,6 +14,7 @@ namespace Avalonia.Diagnostics.Views
{
private static readonly StyledProperty<ControlDetailsViewModel> ViewModelProperty =
AvaloniaProperty.Register<ControlDetailsView, ControlDetailsViewModel>(nameof(ViewModel));
private SimpleGrid _grid;
public ControlDetailsView()
@ -26,7 +26,7 @@ namespace Avalonia.Diagnostics.Views
public ControlDetailsViewModel ViewModel
{
get { return GetValue(ViewModelProperty); }
get => GetValue(ViewModelProperty);
private set
{
SetValue(ViewModelProperty, value);
@ -38,48 +38,37 @@ namespace Avalonia.Diagnostics.Views
{
Func<object, IEnumerable<Control>> pt = PropertyTemplate;
Content = new ScrollViewer
{
Content = _grid = new SimpleGrid
{
Styles =
{
new Style(x => x.Is<Control>())
{
Setters = new[]
{
new Setter(MarginProperty, new Thickness(2)),
}
},
},
[GridRepeater.TemplateProperty] = pt,
}
};
Content = new ScrollViewer { Content = _grid = new SimpleGrid { [GridRepeater.TemplateProperty] = pt } };
}
private IEnumerable<Control> PropertyTemplate(object i)
{
var property = (PropertyDetails)i;
var margin = new Thickness(2);
yield return new TextBlock
{
Margin = margin,
Text = property.Name,
TextWrapping = TextWrapping.NoWrap,
[!ToolTip.TipProperty] = property.GetObservable<string>(nameof(property.Diagnostic)).ToBinding(),
[!ToolTip.TipProperty] = property.GetObservable<string>(nameof(property.Diagnostic)).ToBinding()
};
yield return new TextBlock
{
Margin = margin,
TextWrapping = TextWrapping.NoWrap,
[!TextBlock.TextProperty] = property.GetObservable<object>(nameof(property.Value))
.Select(v => v?.ToString())
.ToBinding(),
.ToBinding()
};
yield return new TextBlock
{
Margin = margin,
TextWrapping = TextWrapping.NoWrap,
[!TextBlock.TextProperty] = property.GetObservable<string>((nameof(property.Priority))).ToBinding(),
[!TextBlock.TextProperty] = property.GetObservable<string>((nameof(property.Priority))).ToBinding()
};
}
}

100
src/Avalonia.Diagnostics/Views/EventsView.xaml

@ -2,53 +2,57 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
x:Class="Avalonia.Diagnostics.Views.EventsView">
<UserControl.Resources>
<vm:BoolToBrushConverter x:Key="boolToBrush" />
</UserControl.Resources>
<Grid ColumnDefinitions="*,4,3*">
<TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" Grid.RowSpan="2">
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:EventTreeNodeBase"
ItemsSource="{Binding Children}">
<CheckBox Content="{Binding Text}" IsChecked="{Binding IsEnabled, Mode=TwoWay}" />
</TreeDataTemplate>
</TreeView.DataTemplates>
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.Styles>
</TreeView>
<GridSplitter Width="4" Grid.Column="1" />
<Grid RowDefinitions="*,4,2*,Auto" Grid.Column="2">
<ListBox Name="eventsList" Items="{Binding RecordedEvents}" SelectedItem="{Binding SelectedEvent, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Background="{Binding IsHandled, Converter={StaticResource boolToBrush}}" Text="{Binding DisplayText}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Height="4" Grid.Row="1" />
<DockPanel Grid.Row="2" LastChildFill="True">
<TextBlock DockPanel.Dock="Top" FontSize="16" Text="Event chain:" />
<ListBox Items="{Binding SelectedEvent.EventChain}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Background="{Binding Handled, Converter={StaticResource boolToBrush}}">
<TextBlock Text="{Binding Route}" />
<TextBlock Text=": " />
<TextBlock Text="{Binding HandlerName}" />
<TextBlock Text=" handled: " />
<TextBlock Text="{Binding Handled}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<StackPanel Orientation="Horizontal" Grid.Row="3">
<Button Content="Clear" Margin="3" Command="{Binding Clear}" />
</StackPanel>
</Grid>
<UserControl.Resources>
<vm:BoolToBrushConverter x:Key="boolToBrush" />
</UserControl.Resources>
<Grid ColumnDefinitions="*,4,3*">
<TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}"
Grid.RowSpan="2">
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:EventTreeNodeBase"
ItemsSource="{Binding Children}">
<CheckBox Content="{Binding Text}" IsChecked="{Binding IsEnabled, Mode=TwoWay}" />
</TreeDataTemplate>
</TreeView.DataTemplates>
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.Styles>
</TreeView>
<GridSplitter Width="4" Grid.Column="1" />
<Grid RowDefinitions="*,4,2*,Auto" Grid.Column="2">
<ListBox Name="eventsList" Items="{Binding RecordedEvents}"
SelectedItem="{Binding SelectedEvent, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Background="{Binding IsHandled, Converter={StaticResource boolToBrush}}"
Text="{Binding DisplayText}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Height="4" Grid.Row="1" />
<DockPanel Grid.Row="2" LastChildFill="True">
<TextBlock DockPanel.Dock="Top" FontSize="16" Text="Event chain:" />
<ListBox Items="{Binding SelectedEvent.EventChain}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal"
Background="{Binding Handled, Converter={StaticResource boolToBrush}}">
<TextBlock Text="{Binding Route}" />
<TextBlock Text=": " />
<TextBlock Text="{Binding HandlerName}" />
<TextBlock Text=" handled: " />
<TextBlock Text="{Binding Handled}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<StackPanel Orientation="Horizontal" Grid.Row="3">
<Button Content="Clear" Margin="3" Command="{Binding Clear}" />
</StackPanel>
</Grid>
</Grid>
</UserControl>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save