Browse Source

Merge branch 'master' into fix/SplitViewOverlay

amwx 2 years ago
parent
commit
1207673145
100 changed files with 1344 additions and 1071 deletions
  1. 5 0
      .ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject
  2. 5 0
      .ncrunch/Generators.Sandbox.v3.ncrunchproject
  3. 7 3
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  4. 1 1
      samples/ControlCatalog/MainView.xaml
  5. 49 11
      samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
  6. 1 1
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  7. 1 1
      samples/ControlCatalog/Pages/CompositionPage.axaml.cs
  8. 1 1
      samples/ControlCatalog/Pages/ContextFlyoutPage.xaml
  9. 2 2
      samples/ControlCatalog/Pages/ContextMenuPage.xaml
  10. 1 1
      samples/ControlCatalog/Pages/CursorPage.xaml
  11. 4 4
      samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs
  12. 6 6
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  13. 1 1
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  14. 3 3
      samples/ControlCatalog/Pages/MenuPage.xaml
  15. 4 4
      samples/ControlCatalog/Pages/NativeEmbedPage.xaml.cs
  16. 3 3
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  17. 1 1
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml
  18. 2 2
      samples/ControlCatalog/Pages/ScrollSnapPage.xaml
  19. 2 2
      samples/ControlCatalog/Pages/ScrollViewerPage.xaml
  20. 1 1
      samples/ControlCatalog/Pages/TabStripPage.xaml
  21. 1 1
      samples/ControlCatalog/Pages/ThemePage.axaml.cs
  22. 1 1
      samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml
  23. 1 1
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  24. 7 1
      samples/IntegrationTestApp/MainWindow.axaml
  25. 5 1
      samples/IntegrationTestApp/MainWindow.axaml.cs
  26. 18 6
      samples/IntegrationTestApp/ShowWindowTest.axaml
  27. 1 1
      samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
  28. 1 1
      samples/SampleControls/HamburgerMenu/HamburgerMenu.cs
  29. 0 9
      src/Avalonia.Base/CornerRadius.cs
  30. 1 1
      src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs
  31. 1 10
      src/Avalonia.Base/Media/BoxShadow.cs
  32. 2 2
      src/Avalonia.Base/Media/BoxShadows.cs
  33. 1 1
      src/Avalonia.Base/Media/DrawingGroup.cs
  34. 0 5
      src/Avalonia.Base/Media/FontFamily.cs
  35. 3 2
      src/Avalonia.Base/Media/FormattedText.cs
  36. 1 1
      src/Avalonia.Base/Media/ImageDrawing.cs
  37. 1 1
      src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs
  38. 1 1
      src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs
  39. 2 17
      src/Avalonia.Base/PixelRect.cs
  40. 16 1
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  41. 13 3
      src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
  42. 0 8
      src/Avalonia.Base/Point.cs
  43. 1 1
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  44. 8 26
      src/Avalonia.Base/Rect.cs
  45. 6 6
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  46. 25 30
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  47. 1 1
      src/Avalonia.Base/Rendering/DirtyRects.cs
  48. 2 2
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  49. 0 11
      src/Avalonia.Base/Size.cs
  50. 1 13
      src/Avalonia.Base/StyledProperty.cs
  51. 0 10
      src/Avalonia.Base/Thickness.cs
  52. 0 8
      src/Avalonia.Base/Vector.cs
  53. 1 1
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  54. 1 1
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  55. 1 1
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
  56. 1 1
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml
  57. 4 4
      src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
  58. 4 1
      src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
  59. 2 2
      src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs
  60. 5 7
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs
  61. 1 2
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs
  62. 1 1
      src/Avalonia.Controls/BorderVisual.cs
  63. 20 0
      src/Avalonia.Controls/ContainerClearingEventArgs.cs
  64. 32 0
      src/Avalonia.Controls/ContainerIndexChangedEventArgs.cs
  65. 26 0
      src/Avalonia.Controls/ContainerPreparedEventArgs.cs
  66. 45 58
      src/Avalonia.Controls/ContextMenu.cs
  67. 1 1
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  68. 1 1
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  69. 4 4
      src/Avalonia.Controls/Documents/InlineCollection.cs
  70. 3 1
      src/Avalonia.Controls/Flyouts/MenuFlyout.cs
  71. 7 9
      src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs
  72. 165 0
      src/Avalonia.Controls/ItemCollection.cs
  73. 160 83
      src/Avalonia.Controls/ItemsControl.cs
  74. 91 64
      src/Avalonia.Controls/ItemsSourceView.cs
  75. 2 2
      src/Avalonia.Controls/LayoutTransformControl.cs
  76. 66 69
      src/Avalonia.Controls/MaskedTextBox.cs
  77. 1 1
      src/Avalonia.Controls/NativeControlHost.cs
  78. 1 17
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  79. 31 78
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  80. 47 33
      src/Avalonia.Controls/Primitives/Popup.cs
  81. 2 1
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  82. 29 26
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  83. 46 61
      src/Avalonia.Controls/SelectableTextBlock.cs
  84. 26 1
      src/Avalonia.Controls/Selection/InternalSelectionModel.cs
  85. 2 2
      src/Avalonia.Controls/Selection/SelectionModel.cs
  86. 1 1
      src/Avalonia.Controls/SplitButton/SplitButton.cs
  87. 37 29
      src/Avalonia.Controls/TextBlock.cs
  88. 213 220
      src/Avalonia.Controls/TextBox.cs
  89. 11 8
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  90. 0 1
      src/Avalonia.Controls/TopLevel.cs
  91. 2 2
      src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs
  92. 3 1
      src/Avalonia.Controls/Utils/UndoRedoHelper.cs
  93. 3 17
      src/Avalonia.Controls/VirtualizingPanel.cs
  94. 3 2
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  95. 12 17
      src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs
  96. 1 1
      src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml
  97. 3 3
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  98. 3 3
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
  99. 1 1
      src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs
  100. 1 1
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml

+ 5 - 0
.ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
+  </Settings>
+</ProjectConfiguration>

+ 5 - 0
.ncrunch/Generators.Sandbox.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
+  </Settings>
+</ProjectConfiguration>

+ 7 - 3
native/Avalonia.Native/src/OSX/WindowImpl.mm

@@ -281,10 +281,13 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) {
 
             case SystemDecorationsFull:
                 [Window setHasShadow:YES];
-                [Window setTitleVisibility:NSWindowTitleVisible];
-                [Window setTitlebarAppearsTransparent:NO];
                 [Window setTitle:_lastTitle];
 
+                if (!_isClientAreaExtended) {
+                    [Window setTitleVisibility:NSWindowTitleVisible];
+                    [Window setTitlebarAppearsTransparent:NO];
+                }
+
                 if (currentWindowState == Maximized) {
                     auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
 
@@ -611,7 +614,8 @@ void WindowImpl::UpdateStyle() {
     }
 
     bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
-    bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull;
+    bool hasTrafficLights = (_decorations == SystemDecorationsFull) &&
+        (_isClientAreaExtended ? wantsChrome : true);
     
     NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton];
     NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton];

+ 1 - 1
samples/ControlCatalog/MainView.xaml

@@ -241,7 +241,7 @@
               </ComboBox.Items>
             </ComboBox>
             <ComboBox HorizontalAlignment="Stretch"
-                      Items="{Binding WindowStates}"
+                      ItemsSource="{Binding WindowStates}"
                       SelectedItem="{Binding WindowState}" />
           </StackPanel>
         </Flyout>

+ 49 - 11
samples/ControlCatalog/Pages/ClipboardPage.xaml.cs

@@ -1,16 +1,23 @@
 using System;
 using System.Collections.Generic;
-
+using System.Linq;
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.Notifications;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
+using Avalonia.Platform;
+using Avalonia.Platform.Storage;
+using Avalonia.Platform.Storage.FileIO;
 
 namespace ControlCatalog.Pages
 {
     public partial class ClipboardPage : UserControl
     {
+        private INotificationManager? _notificationManager;
+        private INotificationManager NotificationManager => _notificationManager
+            ??= new WindowNotificationManager(TopLevel.GetTopLevel(this)!);
         public ClipboardPage()
         {
             InitializeComponent();
@@ -31,7 +38,7 @@ namespace ControlCatalog.Pages
 
         private async void PasteText(object? sender, RoutedEventArgs args)
         {
-            if(Application.Current!.Clipboard is { } clipboard)
+            if (Application.Current!.Clipboard is { } clipboard)
             {
                 ClipboardContent.Text = await clipboard.GetTextAsync();
             }
@@ -59,15 +66,45 @@ namespace ControlCatalog.Pages
         {
             if (Application.Current!.Clipboard is { } clipboard)
             {
-                var files = (ClipboardContent.Text ?? String.Empty)
-                .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
-                if (files.Length == 0)
+                var storageProvider = TopLevel.GetTopLevel(this)!.StorageProvider;
+                var filesPath = (ClipboardContent.Text ?? string.Empty)
+                    .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
+                if (filesPath.Length == 0)
                 {
                     return;
                 }
-                var dataObject = new DataObject();
-                dataObject.Set(DataFormats.FileNames, files);
-                await clipboard.SetDataObjectAsync(dataObject);
+                List<string> invalidFile = new(filesPath.Length);
+                List<IStorageFile> files = new(filesPath.Length);
+
+                for (int i = 0; i < filesPath.Length; i++)
+                {
+                    var file = await storageProvider.TryGetFileFromPathAsync(filesPath[i]);
+                    if (file is null)
+                    {
+                        invalidFile.Add(filesPath[i]);
+                    }
+                    else
+                    {
+                        files.Add(file);
+                    }
+                }
+
+                if (invalidFile.Count > 0)
+                {
+                    NotificationManager.Show(new Notification("Warning", "There is one o more invalid path.", NotificationType.Warning));
+                }
+
+                if (files.Count > 0)
+                {
+                    var dataObject = new DataObject();
+                    dataObject.Set(DataFormats.Files, files);
+                    await clipboard.SetDataObjectAsync(dataObject);
+                    NotificationManager.Show(new Notification("Success", "Copy completated.", NotificationType.Success));
+                }
+                else
+                {
+                    NotificationManager.Show(new Notification("Warning", "Any files to copy in Clipboard.", NotificationType.Warning));
+                }
             }
         }
 
@@ -75,8 +112,9 @@ namespace ControlCatalog.Pages
         {
             if (Application.Current!.Clipboard is { } clipboard)
             {
-                var fiels = await clipboard.GetDataAsync(DataFormats.FileNames) as IEnumerable<string>;
-                ClipboardContent.Text = fiels != null ? string.Join(Environment.NewLine, fiels) : string.Empty;
+                var files = await clipboard.GetDataAsync(DataFormats.Files) as IEnumerable<Avalonia.Platform.Storage.IStorageItem>;
+
+                ClipboardContent.Text = files != null ? string.Join(Environment.NewLine, files.Select(f => f.TryGetLocalPath() ?? f.Name)) : string.Empty;
             }
         }
 
@@ -95,7 +133,7 @@ namespace ControlCatalog.Pages
             {
                 await clipboard.ClearAsync();
             }
-                
+
         }
     }
 }

+ 1 - 1
samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs

@@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
         {
             AvaloniaXamlLoader.Load(this);
             var fontComboBox = this.Get<ComboBox>("fontComboBox");
-            fontComboBox.Items = FontManager.Current.SystemFonts;
+            fontComboBox.ItemsSource = FontManager.Current.SystemFonts;
             fontComboBox.SelectedIndex = 0;
         }
     }

+ 1 - 1
samples/ControlCatalog/Pages/CompositionPage.axaml.cs

@@ -32,7 +32,7 @@ public partial class CompositionPage : UserControl
     protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
     {
         base.OnAttachedToVisualTree(e);
-        this.Get<ItemsControl>("Items").Items = CreateColorItems();
+        this.Get<ItemsControl>("Items").ItemsSource = CreateColorItems();
 
     }
 

+ 1 - 1
samples/ControlCatalog/Pages/ContextFlyoutPage.xaml

@@ -61,7 +61,7 @@
         <Border.Styles>
           <Style Selector="MenuFlyoutPresenter MenuItem" x:DataType="viewModels:MenuItemViewModel">
             <Setter Property="Header" Value="{Binding Header}"/>
-            <Setter Property="Items" Value="{Binding Items}"/>
+            <Setter Property="ItemsSource" Value="{Binding Items}"/>
             <Setter Property="Command" Value="{Binding Command}"/>
             <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
           </Style>

+ 2 - 2
samples/ControlCatalog/Pages/ContextMenuPage.xaml

@@ -51,13 +51,13 @@
         <Border.Styles>
           <Style Selector="ContextMenu MenuItem" x:DataType="viewModels:MenuItemViewModel">
             <Setter Property="Header" Value="{Binding Header}"/>
-            <Setter Property="Items" Value="{Binding Items}"/>
+            <Setter Property="ItemsSource" Value="{Binding Items}"/>
             <Setter Property="Command" Value="{Binding Command}"/>
             <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
           </Style>
         </Border.Styles>
         <Border.ContextMenu>
-          <ContextMenu Items="{Binding MenuItems}" />
+          <ContextMenu ItemsSource="{Binding MenuItems}" />
         </Border.ContextMenu>
         <TextBlock Text="Dynamically Generated"/>
       </Border>

+ 1 - 1
samples/ControlCatalog/Pages/CursorPage.xaml

@@ -8,7 +8,7 @@
       <TextBlock Classes="h2">Defines a cursor (mouse pointer)</TextBlock>
     </StackPanel>
 
-    <ListBox Grid.Row="1" Items="{Binding StandardCursors}" Margin="0 8 8 8">
+    <ListBox Grid.Row="1" ItemsSource="{Binding StandardCursors}" Margin="0 8 8 8">
       <ListBox.Styles>
         <Style Selector="ListBoxItem">
           <Setter Property="Cursor" Value="{Binding Cursor}" x:DataType="viewModels:StandardCursorModel"/>

+ 4 - 4
samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs

@@ -133,17 +133,17 @@ namespace ControlCatalog.Pages
 
             // 0,0 refers to the top-left of the control now. It is not prime time to draw gui stuff because it'll be under the world 
 
-            var translateModifier = context.PushPreTransform(Avalonia.Matrix.CreateTranslation(new Avalonia.Vector(halfWidth, halfHeight)));
+            var translateModifier = context.PushTransform(Avalonia.Matrix.CreateTranslation(new Avalonia.Vector(halfWidth, halfHeight)));
 
             // now 0,0 refers to the ViewportCenter(X,Y). 
             var rotationMatrix = Avalonia.Matrix.CreateRotation(Rotation);
-            var rotationModifier = context.PushPreTransform(rotationMatrix);
+            var rotationModifier = context.PushTransform(rotationMatrix);
 
             // everything is rotated but not scaled 
 
-            var scaleModifier = context.PushPreTransform(Avalonia.Matrix.CreateScale(Scale, -Scale));
+            var scaleModifier = context.PushTransform(Avalonia.Matrix.CreateScale(Scale, -Scale));
 
-            var mapPositionModifier = context.PushPreTransform(Matrix.CreateTranslation(new Vector(-ViewportCenterX, -ViewportCenterY)));
+            var mapPositionModifier = context.PushTransform(Matrix.CreateTranslation(new Vector(-ViewportCenterX, -ViewportCenterY)));
 
             // now everything is rotated and scaled, and at the right position, now we're drawing strictly in world coordinates
 

+ 6 - 6
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@@ -106,7 +106,7 @@ namespace ControlCatalog.Pages
                     Directory = initialDirectory,
                     InitialFileName = initialFileName
                 }.ShowAsync(GetWindow());
-                results.Items = result;
+                results.ItemsSource = result;
                 resultsVisible.IsVisible = result?.Any() == true;
             };
             this.Get<Button>("OpenMultipleFiles").Click += async delegate
@@ -118,7 +118,7 @@ namespace ControlCatalog.Pages
                     Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
                     AllowMultiple = true
                 }.ShowAsync(GetWindow());
-                results.Items = result;
+                results.ItemsSource = result;
                 resultsVisible.IsVisible = result?.Any() == true;
             };
             this.Get<Button>("SaveFile").Click += async delegate
@@ -132,7 +132,7 @@ namespace ControlCatalog.Pages
                     DefaultExtension = filters?.Any() == true ? "txt" : null,
                     InitialFileName = "test.txt"
                 }.ShowAsync(GetWindow());
-                results.Items = new[] { result };
+                results.ItemsSource = new[] { result };
                 resultsVisible.IsVisible = result != null;
             };
             this.Get<Button>("SelectFolder").Click += async delegate
@@ -149,7 +149,7 @@ namespace ControlCatalog.Pages
                 else
                 {
                     SetFolder(await GetStorageProvider().TryGetFolderFromPathAsync(result));
-                    results.Items = new[] { result };
+                    results.ItemsSource = new[] { result };
                     resultsVisible.IsVisible = true;
                 }
             };
@@ -164,7 +164,7 @@ namespace ControlCatalog.Pages
                 {
                     AllowDirectorySelection = true
                 });
-                results.Items = result;
+                results.ItemsSource = result;
                 resultsVisible.IsVisible = result?.Any() == true;
             };
             this.Get<Button>("DecoratedWindow").Click += delegate
@@ -332,7 +332,7 @@ namespace ControlCatalog.Pages
                     }
                 }
 
-                results.Items = mappedResults;
+                results.ItemsSource = mappedResults;
                 resultsVisible.IsVisible = mappedResults.Any();
             }
         }

+ 1 - 1
samples/ControlCatalog/Pages/ListBoxPage.xaml

@@ -30,7 +30,7 @@
       <Button Command="{Binding RemoveItemCommand}">Remove</Button>
       <Button Command="{Binding SelectRandomItemCommand}">Select Random Item</Button>
     </StackPanel>
-    <ListBox Items="{Binding Items}"
+    <ListBox ItemsSource="{Binding Items}"
              Selection="{Binding Selection}"
              DisplayMemberBinding="{Binding (viewModels:ItemModel).ID, StringFormat='{}Item {0:N0}'}"
              AutoScrollToSelectedItem="{Binding AutoScrollToSelectedItem}"

+ 3 - 3
samples/ControlCatalog/Pages/MenuPage.xaml

@@ -45,11 +45,11 @@
 
             <StackPanel>
                 <TextBlock Classes="h3" Margin="4 8">Dyanamically generated</TextBlock>
-                <Menu Items="{Binding MenuItems}">
+                <Menu ItemsSource="{Binding MenuItems}">
                     <Menu.Styles>
                         <Style Selector="MenuItem" x:DataType="viewModels:MenuItemViewModel">
                             <Setter Property="Header" Value="{Binding Header}"/>
-                            <Setter Property="Items" Value="{Binding Items}"/>
+                            <Setter Property="ItemsSource" Value="{Binding Items}"/>
                             <Setter Property="Command" Value="{Binding Command}"/>
                             <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
                         </Style>
@@ -68,7 +68,7 @@
                         <Separator/>
                         <MenuItem Header="Execu_te Script..." />
                         <Separator/>
-                        <MenuItem Header="_Recent" Items="{Binding RecentItems}">
+                        <MenuItem Header="_Recent" ItemsSource="{Binding RecentItems}">
                             <MenuItem.Styles>
                                 <Style Selector="MenuItem" x:DataType="viewModels:MenuItemViewModel">
                                     <Setter Property="Header" Value="{Binding Header}"/>

+ 4 - 4
samples/ControlCatalog/Pages/NativeEmbedPage.xaml.cs

@@ -33,10 +33,10 @@ namespace ControlCatalog.Pages
         {
             new ContextMenu()
             {
-                Items = new List<MenuItem>
-            {
-                new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" }
-            }
+                Items =
+                {
+                    new MenuItem() { Header = "Test" }, new MenuItem() { Header = "Test" }
+                }
             }.Open((Control)sender);
         }
 

+ 3 - 3
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@@ -27,7 +27,7 @@
       </Grid>
       <Grid Grid.Row="0" Grid.Column="1" Margin="8" ColumnDefinitions="Auto, 120" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
         <TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">FormatString:</TextBlock>
-        <ComboBox Grid.Row="0" Grid.Column="1" Items="{Binding Formats}" SelectedItem="{Binding SelectedFormat}"
+        <ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding Formats}" SelectedItem="{Binding SelectedFormat}"
                   VerticalAlignment="Center" Margin="2">
           <ComboBox.ItemTemplate>
             <DataTemplate>
@@ -41,11 +41,11 @@
         </ComboBox>
 
         <TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">ButtonSpinnerLocation:</TextBlock>
-        <ComboBox Grid.Row="1" Grid.Column="1" Items="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}"
+        <ComboBox Grid.Row="1" Grid.Column="1" ItemsSource="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}"
                   VerticalAlignment="Center" Margin="2"/>
 
         <TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">CultureInfo:</TextBlock>
-        <ComboBox x:Name="CultureSelector" Grid.Row="2" Grid.Column="1" Items="{Binding Cultures}"
+        <ComboBox x:Name="CultureSelector" Grid.Row="2" Grid.Column="1" ItemsSource="{Binding Cultures}"
                   VerticalAlignment="Center" Margin="2"/>
 
         <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">Watermark:</TextBlock>

+ 1 - 1
samples/ControlCatalog/Pages/RefreshContainerPage.axaml

@@ -21,7 +21,7 @@
                       Margin="5">
       <ListBox HorizontalAlignment="Stretch"
                VerticalAlignment="Top"
-               Items="{Binding Items}"/>
+               ItemsSource="{Binding Items}"/>
     </RefreshContainer>
   </DockPanel>
 </UserControl>

+ 2 - 2
samples/ControlCatalog/Pages/ScrollSnapPage.xaml

@@ -16,14 +16,14 @@
         <StackPanel Orientation="Vertical"
                     Spacing="4">
           <TextBlock Text="Snap Point Type" />
-          <ComboBox Items="{Binding AvailableSnapPointsType}"
+          <ComboBox ItemsSource="{Binding AvailableSnapPointsType}"
                     SelectedItem="{Binding SnapPointsType}" />
         </StackPanel>
 
         <StackPanel Orientation="Vertical"
                     Spacing="4">
           <TextBlock Text="Snap Point Alignment" />
-          <ComboBox Items="{Binding AvailableSnapPointsAlignment}"
+          <ComboBox ItemsSource="{Binding AvailableSnapPointsAlignment}"
                     SelectedItem="{Binding SnapPointsAlignment}" />
         </StackPanel>
 

+ 2 - 2
samples/ControlCatalog/Pages/ScrollViewerPage.xaml

@@ -13,12 +13,12 @@
 
         <StackPanel Orientation="Vertical" Spacing="4">
           <TextBlock Text="Horizontal Scroll" />
-          <ComboBox Items="{Binding AvailableVisibility}" SelectedItem="{Binding HorizontalScrollVisibility}" />
+          <ComboBox ItemsSource="{Binding AvailableVisibility}" SelectedItem="{Binding HorizontalScrollVisibility}" />
         </StackPanel>
 
         <StackPanel Orientation="Vertical" Spacing="4">
           <TextBlock Text="Vertical Scroll" />
-          <ComboBox Items="{Binding AvailableVisibility}" SelectedItem="{Binding VerticalScrollVisibility}" />
+          <ComboBox ItemsSource="{Binding AvailableVisibility}" SelectedItem="{Binding VerticalScrollVisibility}" />
         </StackPanel>
       </StackPanel>
 

+ 1 - 1
samples/ControlCatalog/Pages/TabStripPage.xaml

@@ -18,7 +18,7 @@
         <Separator Margin="0 16"/>
 
         <TextBlock Classes="h1">Dynamically generated</TextBlock>
-        <TabStrip Items="{Binding Tabs}">
+        <TabStrip ItemsSource="{Binding Tabs}">
             <TabStrip.Styles>
                 <Style Selector="TabStripItem" x:DataType="viewModels:TabControlPageViewModelItem">
                     <Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>

+ 1 - 1
samples/ControlCatalog/Pages/ThemePage.axaml.cs

@@ -12,7 +12,7 @@ namespace ControlCatalog.Pages
         {
             InitializeComponent();
 
-            Selector.Items = new[]
+            Selector.ItemsSource = new[]
             {
                 ThemeVariant.Default,
                 ThemeVariant.Dark,

+ 1 - 1
samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml

@@ -53,7 +53,7 @@
             
             <StackPanel Margin="5" Spacing="5" Grid.IsSharedSizeScope="True">
                 <HeaderedContentControl Header="Select a transition">
-                     <ComboBox Items="{Binding PageTransitions}" SelectedItem="{Binding SelectedTransition}" />
+                     <ComboBox ItemsSource="{Binding PageTransitions}" SelectedItem="{Binding SelectedTransition}" />
                 </HeaderedContentControl>
                 <HeaderedContentControl Header="Duration">
                      <NumericUpDown Value="{Binding Duration}" Increment="250" Minimum="100" />

+ 1 - 1
samples/ControlCatalog/Pages/TreeViewPage.xaml

@@ -11,7 +11,7 @@
                 HorizontalAlignment="Center"
                 Spacing="16">
       <StackPanel Orientation="Vertical" Spacing="8">
-        <TreeView Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
+        <TreeView ItemsSource="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
           <TreeView.ItemTemplate>
             <TreeDataTemplate ItemsSource="{Binding Children}">
               <TextBlock Text="{Binding Header}"/>

+ 7 - 1
samples/IntegrationTestApp/MainWindow.axaml

@@ -109,7 +109,7 @@
           <StackPanel DockPanel.Dock="Bottom">
             <Button Name="ListBoxSelectionClear">Clear Selection</Button>
           </StackPanel>
-          <ListBox Name="BasicListBox" Items="{Binding ListBoxItems}" SelectionMode="Multiple"/>
+          <ListBox Name="BasicListBox" ItemsSource="{Binding ListBoxItems}" SelectionMode="Multiple"/>
         </DockPanel>
       </TabItem>
       
@@ -151,6 +151,12 @@
               <ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem>
               <ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem>
             </ComboBox>
+            <ComboBox Name="ShowWindowSystemDecorations" SelectedIndex="2">
+              <ComboBoxItem Name="ShowWindowSystemDecorationsNone">None</ComboBoxItem>
+              <ComboBoxItem Name="ShowWindowSystemDecorationsBorderOnly">BorderOnly</ComboBoxItem>
+              <ComboBoxItem Name="ShowWindowSystemDecorationsFull">Full</ComboBoxItem>
+            </ComboBox>
+            <CheckBox Name="ShowWindowExtendClientAreaToDecorationsHint">ExtendClientAreaToDecorationsHint</CheckBox>
             <CheckBox Name="ShowWindowCanResize" IsChecked="True">Can Resize</CheckBox>
             <Button Name="ShowWindow">Show Window</Button>
             <Button Name="SendToBack">Send to Back</Button>

+ 5 - 1
samples/IntegrationTestApp/MainWindow.axaml.cs

@@ -68,6 +68,8 @@ namespace IntegrationTestApp
             var locationComboBox = this.GetControl<ComboBox>("ShowWindowLocation");
             var stateComboBox = this.GetControl<ComboBox>("ShowWindowState");
             var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null;
+            var systemDecorations = this.GetControl<ComboBox>("ShowWindowSystemDecorations");
+            var extendClientArea = this.GetControl<CheckBox>("ShowWindowExtendClientAreaToDecorationsHint");
             var canResizeCheckBox = this.GetControl<CheckBox>("ShowWindowCanResize");
             var owner = (Window)this.GetVisualRoot()!;
 
@@ -95,6 +97,8 @@ namespace IntegrationTestApp
             }
 
             sizeTextBox.Text = string.Empty;
+            window.ExtendClientAreaToDecorationsHint = extendClientArea.IsChecked ?? false;
+            window.SystemDecorations = (SystemDecorations)systemDecorations.SelectedIndex;
             window.WindowState = (WindowState)stateComboBox.SelectedIndex;
 
             switch (modeComboBox.SelectedIndex)
@@ -158,7 +162,7 @@ namespace IntegrationTestApp
             var popup = new Popup
             {
                 WindowManagerAddShadowHint = false,
-                PlacementMode = PlacementMode.AnchorAndGravity,
+                Placement = PlacementMode.AnchorAndGravity,
                 PlacementAnchor = PopupAnchor.Top,
                 PlacementGravity = PopupGravity.Bottom,
                 Width= 200,

+ 18 - 6
samples/IntegrationTestApp/ShowWindowTest.axaml

@@ -6,7 +6,7 @@
         x:DataType="Window"
         Title="Show Window Test">
   <integrationTestApp:MeasureBorder Name="MyBorder">
-    <Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
+    <Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
       <Label Grid.Column="0" Grid.Row="1">Client Size</Label>
       <TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
                Text="{Binding ClientSize, Mode=OneWay}" />
@@ -35,13 +35,25 @@
         <ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
       </ComboBox>
 
-      <Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
-      <TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="8" IsReadOnly="True" />
+      <Label Grid.Column="0" Grid.Row="8">SystemDecorations</Label>
+      <ComboBox Name="CurrentSystemDecorations" Grid.Column="1" Grid.Row="8"  SelectedIndex="{Binding SystemDecorations}">
+        <ComboBoxItem Name="SystemDecorationsNone">None</ComboBoxItem>
+        <ComboBoxItem Name="SystemDecorationsBorderOnly">BorderOnly</ComboBoxItem>
+        <ComboBoxItem Name="SystemDecorationsFull">Full</ComboBoxItem>
+      </ComboBox>
+
+      <CheckBox Name="CurrentExtendClientAreaToDecorationsHint" Grid.ColumnSpan="2" Grid.Row="9"
+                IsChecked="{Binding ExtendClientAreaToDecorationsHint}">
+        ExtendClientAreaToDecorationsHint
+      </CheckBox>
+
+      <Label Grid.Column="0" Grid.Row="10">Order (mac)</Label>
+      <TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="10" IsReadOnly="True" />
       
-      <Label Grid.Row="9" Content="MeasuredWith:" />
-      <TextBlock Grid.Column="1" Grid.Row="9" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
+      <Label Grid.Row="11" Content="MeasuredWith:" />
+      <TextBlock Grid.Column="1" Grid.Row="11" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
 
-      <Button Name="HideButton" Grid.Row="10" Command="{Binding $parent[Window].Hide}">Hide</Button>
+      <Button Name="HideButton" Grid.Row="12" Command="{Binding $parent[Window].Hide}">Hide</Button>
       
     </Grid>
   </integrationTestApp:MeasureBorder>

+ 1 - 1
samples/RenderDemo/Pages/RenderTargetBitmapPage.cs

@@ -29,7 +29,7 @@ namespace RenderDemo.Pages
         public override void Render(DrawingContext context)
         {
             using (var ctx = _bitmap.CreateDrawingContext())
-            using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100)
+            using (ctx.PushTransform(Matrix.CreateTranslation(-100, -100)
                                          * Matrix.CreateRotation(_st.Elapsed.TotalSeconds)
                                          * Matrix.CreateTranslation(100, 100)))
             {

+ 1 - 1
samples/SampleControls/HamburgerMenu/HamburgerMenu.cs

@@ -57,7 +57,7 @@ namespace ControlSamples
             {
                 if (_splitView is not null && _splitView.DisplayMode == SplitViewDisplayMode.Overlay)
                 {
-                    _splitView.SetValue(SplitView.IsPaneOpenProperty, false, Avalonia.Data.BindingPriority.Animation);
+                    _splitView.SetCurrentValue(SplitView.IsPaneOpenProperty, false);
                 }
             }
         }

+ 0 - 9
src/Avalonia.Base/CornerRadius.cs

@@ -60,15 +60,6 @@ namespace Avalonia
         /// </summary>
         public double BottomLeft { get; }
 
-        /// <summary>
-        /// Gets a value indicating whether the instance has default values (all corner radii are set to 0).
-        /// </summary>
-        public bool IsDefault => TopLeft == 0 && TopRight == 0 && BottomLeft == 0 && BottomRight == 0;
-
-        /// <inheritdoc cref="IsDefault"/>
-        [Obsolete("Use IsDefault instead.")]
-        public bool IsEmpty => IsDefault;
-
         /// <summary>
         /// Gets a value indicating whether all corner radii are equal.
         /// </summary>

+ 1 - 1
src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs

@@ -180,7 +180,7 @@ namespace Avalonia.Input.GestureRecognizers
         internal Velocity GetVelocity()
         {
             var estimate = GetVelocityEstimate();
-            if (estimate == null || estimate.PixelsPerSecond.IsDefault)
+            if (estimate == null || estimate.PixelsPerSecond == default(Vector))
             {
                 return new Velocity(Vector.Zero);
             }

+ 1 - 10
src/Avalonia.Base/Media/BoxShadow.cs

@@ -45,15 +45,6 @@ namespace Avalonia.Media
             }
         }
 
-        /// <summary>
-        /// Gets a value indicating whether the instance has default values.
-        /// </summary>
-        public bool IsDefault => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0;
-
-        /// <inheritdoc cref="IsDefault"/>
-        [Obsolete("Use IsDefault instead.")]
-        public bool IsEmpty => IsDefault;
-
         private readonly static char[] s_Separator = new char[] { ' ', '\t' };
 
         struct ArrayReader
@@ -89,7 +80,7 @@ namespace Avalonia.Media
         {
             var sb = StringBuilderCache.Acquire();
 
-            if (IsDefault)
+            if (this == default)
             {
                 return "none";
             }

+ 2 - 2
src/Avalonia.Base/Media/BoxShadows.cs

@@ -21,7 +21,7 @@ namespace Avalonia.Media
         {
             _first = shadow;
             _list = null;
-            Count = _first.IsDefault ? 0 : 1;
+            Count = _first == default ? 0 : 1;
         }
 
         public BoxShadows(BoxShadow first, BoxShadow[] rest)
@@ -120,7 +120,7 @@ namespace Avalonia.Media
             get
             {
                 foreach(var boxShadow in this)
-                    if (!boxShadow.IsDefault && boxShadow.IsInset)
+                    if (boxShadow != default && boxShadow.IsInset)
                         return true;
                 return false;
             }

+ 1 - 1
src/Avalonia.Base/Media/DrawingGroup.cs

@@ -73,7 +73,7 @@ namespace Avalonia.Media
         {
             var bounds = GetBounds();
 
-            using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity))
+            using (context.PushTransform(Transform?.Value ?? Matrix.Identity))
             using (context.PushOpacity(Opacity, bounds))
             using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default)
             using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default)

+ 0 - 5
src/Avalonia.Base/Media/FontFamily.cs

@@ -79,11 +79,6 @@ namespace Avalonia.Media
         /// <remarks>Key is only used for custom fonts.</remarks>
         public FontFamilyKey? Key { get; }
 
-        /// <summary>
-        /// Returns <c>True</c> if this instance is the system's default.
-        /// </summary>
-        public bool IsDefault => Name.Equals(DefaultFontFamilyName);
-
         /// <summary>
         /// Implicit conversion of string to FontFamily
         /// </summary>

+ 3 - 2
src/Avalonia.Base/Media/FormattedText.cs

@@ -1393,10 +1393,11 @@ namespace Avalonia.Media
                 }
             }
 
-            if (accumulatedBounds?.PlatformImpl == null || accumulatedBounds.PlatformImpl.Bounds.IsDefault)
+            if (accumulatedBounds?.PlatformImpl == null ||
+                (accumulatedBounds.PlatformImpl.Bounds.Width == 0 && accumulatedBounds.PlatformImpl.Bounds.Height == 0))
             {
                 return null;
-            }            
+            }
 
             return accumulatedBounds;
         }

+ 1 - 1
src/Avalonia.Base/Media/ImageDrawing.cs

@@ -42,7 +42,7 @@ namespace Avalonia.Media
             var imageSource = ImageSource;
             var rect = Rect;
 
-            if (imageSource is object && !rect.IsDefault)
+            if (imageSource is object && (rect.Width != 0 || rect.Height != 0))
             {
                 context.DrawImage(imageSource, rect);
             }

+ 1 - 1
src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs

@@ -77,7 +77,7 @@ namespace Avalonia.Media.Imaging
             {
                 if (Source is not IBitmap bmp)
                     return default;
-                if (SourceRect.IsDefault)
+                if (SourceRect.Width == 0 && SourceRect.Height == 0)
                     return Source.Size;
                 return SourceRect.Size.ToSizeWithDpi(bmp.Dpi);
             }

+ 1 - 1
src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs

@@ -9,7 +9,7 @@ namespace Avalonia.Metadata;
 /// A typical usage example is a ListBox control, where <see cref="InheritDataTypeFromItemsAttribute"/> is defined on the ItemTemplate property,
 /// allowing the template to inherit the data type from the Items collection binding. 
 /// </remarks>
-[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
 public sealed class InheritDataTypeFromItemsAttribute : Attribute
 {
     /// <summary>

+ 2 - 17
src/Avalonia.Base/PixelRect.cs

@@ -9,12 +9,6 @@ namespace Avalonia
     /// </summary>
     public readonly struct PixelRect : IEquatable<PixelRect>
     {
-        /// <summary>
-        /// An empty rectangle.
-        /// </summary>
-        [Obsolete("Use the default keyword instead.")]
-        public static readonly PixelRect Empty = default;
-
         /// <summary>
         /// Initializes a new instance of the <see cref="PixelRect"/> structure.
         /// </summary>
@@ -133,15 +127,6 @@ namespace Avalonia
         /// </summary>
         public PixelPoint Center => new PixelPoint(X + (Width / 2), Y + (Height / 2));
 
-        /// <summary>
-        /// Gets a value indicating whether the instance has default values (the rectangle is empty).
-        /// </summary>
-        public bool IsDefault => Width == 0 && Height == 0;
-
-        /// <inheritdoc cref="IsDefault"/>
-        [Obsolete("Use IsDefault instead.")]
-        public bool IsEmpty => IsDefault;
-
         /// <summary>
         /// Checks for equality between two <see cref="PixelRect"/>s.
         /// </summary>
@@ -295,11 +280,11 @@ namespace Avalonia
         /// <returns>The union.</returns>
         public PixelRect Union(PixelRect rect)
         {
-            if (IsDefault)
+            if (Width == 0 && Height == 0)
             {
                 return rect;
             }
-            else if (rect.IsDefault)
+            else if (rect.Width == 0 && rect.Height == 0)
             {
                 return this;
             }

+ 16 - 1
src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
 using System.Text;
@@ -23,7 +24,7 @@ internal static class StorageProviderHelpers
 
         return null;
     }
-    
+
     public static Uri FilePathToUri(string path)
     {
         var uriPath = new StringBuilder(path)
@@ -35,6 +36,20 @@ internal static class StorageProviderHelpers
         return new UriBuilder("file", string.Empty) { Path = uriPath }.Uri;
     }
     
+    public static bool TryFilePathToUri(string path, [NotNullWhen(true)] out Uri? uri)
+    {
+        try
+        {
+            uri = FilePathToUri(path);
+            return true;
+        }
+        catch
+        {
+            uri = null;
+            return false;
+        }
+    }
+    
     public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter)
     {
         var name = Path.GetFileName(path);

+ 13 - 3
src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs

@@ -16,8 +16,13 @@ public static class StorageProviderExtensions
         {
             return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(filePath) as IStorageFile);
         }
-        
-        return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath));
+
+        if (StorageProviderHelpers.TryFilePathToUri(filePath, out var uri))
+        {
+            return provider.TryGetFileFromPathAsync(uri);
+        }
+
+        return Task.FromResult<IStorageFile?>(null);
     }
 
     /// <inheritdoc cref="IStorageProvider.TryGetFolderFromPathAsync"/>
@@ -29,7 +34,12 @@ public static class StorageProviderExtensions
             return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(folderPath) as IStorageFolder);
         }
 
-        return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath));
+        if (StorageProviderHelpers.TryFilePathToUri(folderPath, out var uri))
+        {
+            return provider.TryGetFolderFromPathAsync(uri);
+        }
+
+        return Task.FromResult<IStorageFolder?>(null);
     }
 
     /// <summary>

+ 0 - 8
src/Avalonia.Base/Point.cs

@@ -288,13 +288,5 @@ namespace Avalonia
             x = this._x;
             y = this._y;
         }
-
-        /// <summary>
-        /// Gets a value indicating whether the X and Y coordinates are zero.
-        /// </summary>
-        public bool IsDefault
-        {
-            get { return (_x == 0) && (_y == 0); }
-        }
     }
 }

+ 1 - 1
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@@ -31,7 +31,7 @@ namespace Avalonia.PropertyStore
 
             var value = inherited is null ? _metadata.DefaultValue : inherited.Value;
 
-            if (property.HasCoercion && _metadata.CoerceValue is { } coerce)
+            if (_metadata.CoerceValue is { } coerce)
             {
                 _uncommon = new()
                 {

+ 8 - 26
src/Avalonia.Base/Rect.cs

@@ -16,12 +16,6 @@ namespace Avalonia
             Animation.Animation.RegisterAnimator<RectAnimator>(prop => typeof(Rect).IsAssignableFrom(prop.PropertyType));
         }
 
-        /// <summary>
-        /// An empty rectangle.
-        /// </summary>
-        [Obsolete("Use the default keyword instead.")]
-        public static readonly Rect Empty = default;
-
         /// <summary>
         /// The X position.
         /// </summary>
@@ -170,17 +164,6 @@ namespace Avalonia
         /// </summary>
         public Point Center => new Point(_x + (_width / 2), _y + (_height / 2));
 
-        /// <summary>
-        /// Gets a value indicating whether the instance has default values (the rectangle is empty).
-        /// </summary>
-        // ReSharper disable CompareOfFloatsByEqualityOperator
-        public bool IsDefault => _width == 0 && _height == 0;
-        // ReSharper restore CompareOfFloatsByEqualityOperator
-
-        /// <inheritdoc cref="IsDefault"/>
-        [Obsolete("Use IsDefault instead.")]
-        public bool IsEmpty => IsDefault;
-
         /// <summary>
         /// Checks for equality between two <see cref="Rect"/>s.
         /// </summary>
@@ -517,19 +500,18 @@ namespace Avalonia
             return rect;
         }
 
-
-            /// <summary>
-            /// Gets the union of two rectangles.
-            /// </summary>
-            /// <param name="rect">The other rectangle.</param>
-            /// <returns>The union.</returns>
-            public Rect Union(Rect rect)
+        /// <summary>
+        /// Gets the union of two rectangles.
+        /// </summary>
+        /// <param name="rect">The other rectangle.</param>
+        /// <returns>The union.</returns>
+        public Rect Union(Rect rect)
         {
-            if (IsDefault)
+            if (Width == 0 && Height == 0)
             {
                 return rect;
             }
-            else if (rect.IsDefault)
+            else if (rect.Width == 0 && rect.Height == 0)
             {
                 return this;
             }

+ 6 - 6
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@@ -130,8 +130,8 @@ namespace Avalonia.Rendering.Composition.Server
             }
 
             _renderTarget ??= _compositor.CreateRenderTarget(_surfaces());
-            
-            if(_dirtyRect.IsDefault && !_redrawRequested)
+
+            if ((_dirtyRect.Width == 0 && _dirtyRect.Height == 0) && !_redrawRequested)
                 return;
 
             Revision++;
@@ -163,7 +163,7 @@ namespace Avalonia.Rendering.Composition.Server
                     _dirtyRect = new Rect(0, 0, layerSize.Width, layerSize.Height);
                 }
 
-                if (!_dirtyRect.IsDefault)
+                if (_dirtyRect.Width != 0 || _dirtyRect.Height != 0)
                 {
                     using (var context = _layer.CreateDrawingContext())
                     {
@@ -260,7 +260,7 @@ namespace Avalonia.Rendering.Composition.Server
         
         public void AddDirtyRect(Rect rect)
         {
-            if(rect.IsDefault)
+            if (rect.Width == 0 && rect.Height == 0)
                 return;
             var snapped = SnapToDevicePixels(rect, Scaling);
             DebugEvents?.RectInvalidated(rect);
@@ -275,7 +275,7 @@ namespace Avalonia.Rendering.Composition.Server
 
         public void Dispose()
         {
-            if(_disposed)
+            if (_disposed)
                 return;
             _disposed = true;
             using (_compositor.RenderInterface.EnsureCurrent())
@@ -302,7 +302,7 @@ namespace Avalonia.Rendering.Composition.Server
         {
             if (_attachedVisuals.Remove(visual) && IsEnabled)
                 visual.Deactivate();
-            if(visual.IsVisibleInFrame)
+            if (visual.IsVisibleInFrame)
                 AddDirtyRect(visual.TransformedOwnContentBounds);
         }
 

+ 25 - 30
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@@ -23,21 +23,20 @@ namespace Avalonia.Rendering.Composition.Server
         private bool _isBackface;
         private Rect? _transformedClipBounds;
         private Rect _combinedTransformedClipBounds;
-        
+
         protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
         {
-            
         }
 
         public void Render(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
         {
-            if(Visible == false || IsVisibleInFrame == false)
+            if (Visible == false || IsVisibleInFrame == false)
                 return;
-            if(Opacity == 0)
+            if (Opacity == 0)
                 return;
 
             currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds);
-            if(currentTransformedClip.IsDefault)
+            if (currentTransformedClip.Width == 0 && currentTransformedClip.Height == 0)
                 return;
 
             Root!.RenderedVisuals++;
@@ -61,7 +60,7 @@ namespace Avalonia.Rendering.Composition.Server
                 canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
             if (Clip != null) 
                 canvas.PushGeometryClip(Clip);
-            if(OpacityMaskBrush != null)
+            if (OpacityMaskBrush != null)
                 canvas.PushOpacityMask(OpacityMaskBrush, boundsRect);
 
             RenderCore(canvas, currentTransformedClip);
@@ -78,12 +77,12 @@ namespace Avalonia.Rendering.Composition.Server
                 canvas.PopClip();
             if (AdornedVisual != null && AdornerIsClipped)
                 canvas.PopClip();
-            if(Opacity != 1)
+            if (Opacity != 1)
                 canvas.PopOpacity();
         }
 
         protected virtual bool HandlesClipToBounds => false;
-        
+
         private ReadbackData _readback0, _readback1, _readback2;
 
         /// <summary>
@@ -98,17 +97,17 @@ namespace Avalonia.Rendering.Composition.Server
                 return ref _readback1;
             return ref _readback2;
         }
-        
+
         public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity;
         public Matrix4x4 GlobalTransformMatrix { get; private set; }
 
         public virtual void Update(ServerCompositionTarget root)
         {
-            if(Parent == null && Root == null)
+            if (Parent == null && Root == null)
                 return;
-            
+
             var wasVisible = IsVisibleInFrame;
-            
+
             // Calculate new parent-relative transform
             if (_combinedTransformDirty)
             {
@@ -122,7 +121,7 @@ namespace Avalonia.Rendering.Composition.Server
             var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix4x4.Identity;
 
             var newTransform = CombinedTransformMatrix * parentTransform;
-            
+
             // Check if visual was moved and recalculate face orientation
             var positionChanged = false;
             if (GlobalTransformMatrix != newTransform)
@@ -134,23 +133,23 @@ namespace Avalonia.Rendering.Composition.Server
 
             var oldTransformedContentBounds = TransformedOwnContentBounds;
             var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds;
-            
+
             if (_parent?.IsDirtyComposition == true)
             {
                 IsDirtyComposition = true;
                 _isDirtyForUpdate = true;
             }
-            
+
             var invalidateOldBounds = _isDirtyForUpdate;
             var invalidateNewBounds = _isDirtyForUpdate;
 
             GlobalTransformMatrix = newTransform;
-            
+
             var ownBounds = OwnContentBounds;
             if (ownBounds != _oldOwnContentBounds || positionChanged)
             {
                 _oldOwnContentBounds = ownBounds;
-                if (ownBounds.IsDefault)
+                if (ownBounds.Width == 0 && ownBounds.Height == 0)
                     TransformedOwnContentBounds = default;
                 else
                     TransformedOwnContentBounds =
@@ -171,16 +170,16 @@ namespace Avalonia.Rendering.Composition.Server
                 AdornedVisual?._combinedTransformedClipBounds
                 ?? Parent?._combinedTransformedClipBounds
                 ?? new Rect(Root!.Size);
-            
+
             if (_transformedClipBounds != null)
                 _combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value);
-            
+
             EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1);
 
             IsHitTestVisibleInFrame = _parent?.IsHitTestVisibleInFrame != false
                                       && Visible
                                       && !_isBackface
-                                      && !_combinedTransformedClipBounds.IsDefault;
+                                      && (_combinedTransformedClipBounds.Width != 0 || _combinedTransformedClipBounds.Height != 0);
 
             IsVisibleInFrame = IsHitTestVisibleInFrame
                                && _parent?.IsVisibleInFrame != false
@@ -213,11 +212,11 @@ namespace Avalonia.Rendering.Composition.Server
 
         void AddDirtyRect(Rect rc)
         {
-            if(rc == default)
+            if (rc == default)
                 return;
             Root?.AddDirtyRect(rc);
         }
-        
+
         /// <summary>
         /// Data that can be read from the UI thread
         /// </summary>
@@ -228,7 +227,7 @@ namespace Avalonia.Rendering.Composition.Server
             public long TargetId;
             public bool Visible;
         }
-        
+
         partial void DeserializeChangesExtra(BatchStreamReader c)
         {
             ValuesInvalidated();
@@ -245,9 +244,8 @@ namespace Avalonia.Rendering.Composition.Server
 
         protected virtual void OnDetachedFromRoot(ServerCompositionTarget target)
         {
-            
         }
-        
+
         partial void OnRootChanged()
         {
             if (Root != null)
@@ -256,12 +254,11 @@ namespace Avalonia.Rendering.Composition.Server
                 OnAttachedToRoot(Root);
             }
         }
-        
+
         protected virtual void OnAttachedToRoot(ServerCompositionTarget target)
         {
-            
         }
-        
+
         protected override void ValuesInvalidated()
         {
             _isDirtyForUpdate = true;
@@ -274,6 +271,4 @@ namespace Avalonia.Rendering.Composition.Server
         public Rect TransformedOwnContentBounds { get; set; }
         public virtual Rect OwnContentBounds => new Rect(0, 0, Size.X, Size.Y);
     }
-
-
 }

+ 1 - 1
src/Avalonia.Base/Rendering/DirtyRects.cs

@@ -30,7 +30,7 @@ namespace Avalonia.Rendering
         /// </remarks>
         public void Add(Rect rect)
         {
-            if (!rect.IsDefault)
+            if (rect.Width != 0 || rect.Height != 0)
             {
                 for (var i = 0; i < _rects.Count; ++i)
                 {

+ 2 - 2
src/Avalonia.Base/Rendering/ImmediateRenderer.cs

@@ -83,7 +83,7 @@ namespace Avalonia.Rendering
                     }
                 }
 
-                using (context.PushPostTransform(m))
+                using (context.PushTransform(m))
                 using (context.PushOpacity(opacity, bounds))
                 using (clipToBounds
 #pragma warning disable CS0618 // Type or member is obsolete
@@ -95,7 +95,7 @@ namespace Avalonia.Rendering
 
                 using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default)
                 using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default)
-                using (context.PushTransformContainer())
+                using (context.PushTransform(Matrix.Identity))
                 {
                     visual.Render(context);
                     

+ 0 - 11
src/Avalonia.Base/Size.cs

@@ -27,12 +27,6 @@ namespace Avalonia
         /// </summary>
         public static readonly Size Infinity = new Size(double.PositiveInfinity, double.PositiveInfinity);
 
-        /// <summary>
-        /// A size representing zero.
-        /// </summary>
-        [Obsolete("Use the default keyword instead.")]
-        public static readonly Size Empty = new Size(0, 0);
-
         /// <summary>
         /// The width.
         /// </summary>
@@ -306,10 +300,5 @@ namespace Avalonia
             width = this._width;
             height = this._height;
         }
-
-        /// <summary>
-        /// Gets a value indicating whether the Width and Height values are zero.
-        /// </summary>
-        public bool IsDefault => (_width == 0) && (_height == 0);
     }
 }

+ 1 - 13
src/Avalonia.Base/StyledProperty.cs

@@ -34,7 +34,6 @@ namespace Avalonia
         {
             Inherits = inherits;
             ValidateValue = validate;
-            HasCoercion |= metadata.CoerceValue != null;
 
             if (validate?.Invoke(metadata.DefaultValue) == false)
             {
@@ -48,12 +47,6 @@ namespace Avalonia
         /// </summary>
         public Func<TValue, bool>? ValidateValue { get; }
 
-        /// <summary>
-        /// Gets a value indicating whether this property has any value coercion callbacks defined
-        /// in its metadata.
-        /// </summary>
-        internal bool HasCoercion { get; private set; }
-
         /// <summary>
         /// Registers the property on another type.
         /// </summary>
@@ -130,10 +123,7 @@ namespace Avalonia
         /// </summary>
         /// <typeparam name="T">The type.</typeparam>
         /// <param name="metadata">The metadata.</param>
-        public void OverrideMetadata<T>(StyledPropertyMetadata<TValue> metadata) where T : AvaloniaObject
-        {
-            base.OverrideMetadata(typeof(T), metadata);
-        }
+        public void OverrideMetadata<T>(StyledPropertyMetadata<TValue> metadata) where T : AvaloniaObject => OverrideMetadata(typeof(T), metadata);
 
         /// <summary>
         /// Overrides the metadata for the property on the specified type.
@@ -151,8 +141,6 @@ namespace Avalonia
                 }
             }
 
-            HasCoercion |= metadata.CoerceValue != null;
-
             base.OverrideMetadata(type, metadata);
         }
 

+ 0 - 10
src/Avalonia.Base/Thickness.cs

@@ -97,10 +97,6 @@ namespace Avalonia
         /// </summary>
         public double Bottom => _bottom;
 
-        /// <inheritdoc cref="IsDefault"/>
-        [Obsolete("Use IsDefault instead.")]
-        public bool IsEmpty => IsDefault;
-
         /// <summary>
         /// Gets a value indicating whether all sides are equal.
         /// </summary>
@@ -293,11 +289,5 @@ namespace Avalonia
             right = this._right;
             bottom = this._bottom;
         }
-
-        /// <summary>
-        /// Gets a value indicating whether the instance has default values
-        /// (the left, top, right and bottom values are zero).
-        /// </summary>
-        public bool IsDefault => (_left == 0) && (_top == 0) && (_right == 0) && (_bottom == 0);
     }
 }

+ 0 - 8
src/Avalonia.Base/Vector.cs

@@ -360,13 +360,5 @@ namespace Avalonia
             x = this._x;
             y = this._y;
         }
-
-        /// <summary>
-        /// Gets a value indicating whether the X and Y components are zero.
-        /// </summary>
-        public bool IsDefault
-        {
-            get { return (_x == 0) && (_y == 0); }
-        }
     }
 }

+ 1 - 1
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml

@@ -165,7 +165,7 @@
                     </TabItem.Header>
                     <ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
                              ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
-                             Items="{TemplateBinding PaletteColors}"
+                             ItemsSource="{TemplateBinding PaletteColors}"
                              SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
                              UseLayoutRounding="False"
                              Margin="12">

+ 1 - 1
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@@ -414,7 +414,7 @@
               </TabItem.Header>
               <ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
                        ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
-                       Items="{TemplateBinding PaletteColors}"
+                       ItemsSource="{TemplateBinding PaletteColors}"
                        SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
                        UseLayoutRounding="False"
                        Margin="12">

+ 1 - 1
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml

@@ -165,7 +165,7 @@
                     </TabItem.Header>
                     <ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
                              ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
-                             Items="{TemplateBinding PaletteColors}"
+                             ItemsSource="{TemplateBinding PaletteColors}"
                              SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
                              UseLayoutRounding="False"
                              Margin="12">

+ 1 - 1
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml

@@ -376,7 +376,7 @@
               </TabItem.Header>
               <ListBox Theme="{StaticResource ColorViewPaletteListBoxTheme}"
                        ItemContainerTheme="{StaticResource ColorViewPaletteListBoxItemTheme}"
-                       Items="{TemplateBinding PaletteColors}"
+                       ItemsSource="{TemplateBinding PaletteColors}"
                        SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
                        UseLayoutRounding="False"
                        Margin="12">

+ 4 - 4
src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs

@@ -177,14 +177,14 @@ namespace Avalonia.Controls
                 }
 
                 bool? uneditedValue = editingCheckBox.IsChecked;
-                if(editingEventArgs is PointerPressedEventArgs args)
+                if (editingEventArgs is PointerPressedEventArgs args)
                 {
                     void ProcessPointerArgs()
                     {
                         // Editing was triggered by a mouse click
                         Point position = args.GetPosition(editingCheckBox);
                         Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
-                        if(rect.Contains(position))
+                        if (rect.Contains(position))
                         {
                             EditValue();
                         }
@@ -192,14 +192,14 @@ namespace Avalonia.Controls
                     
                     void OnLayoutUpdated(object sender, EventArgs e)
                     {
-                        if(!editingCheckBox.Bounds.IsDefault)
+                        if (editingCheckBox.Bounds.Width != 0 || editingCheckBox.Bounds.Height != 0)
                         {
                             editingCheckBox.LayoutUpdated -= OnLayoutUpdated;
                             ProcessPointerArgs();
                         }
                     }
 
-                    if(editingCheckBox.Bounds.IsDefault)
+                    if (editingCheckBox.Bounds.Width == 0 && editingCheckBox.Bounds.Height == 0)
                     {
                         editingCheckBox.LayoutUpdated += OnLayoutUpdated;
                     }

+ 4 - 1
src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs

@@ -39,7 +39,10 @@ namespace Avalonia.Controls
         /// 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);
+            AvaloniaProperty.RegisterDirect<ItemsRepeater, IEnumerable?>(
+                nameof(Items),
+                o => o.Items,
+                (o, v) => o.Items = v);
 
         /// <summary>
         /// Defines the <see cref="Layout"/> property.

+ 2 - 2
src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs

@@ -441,7 +441,7 @@ namespace Avalonia.Controls
 
             _pendingViewportShift = default;
             _unshiftableShift = default;
-            if (_visibleWindow.IsDefault)
+            if (_visibleWindow.Width == 0 && _visibleWindow.Height == 0)
             {
                 // We got cleared.
                 _layoutExtent = default;
@@ -527,7 +527,7 @@ namespace Avalonia.Controls
         private void TryInvalidateMeasure()
         {
             // Don't invalidate measure if we have an invalid window.
-            if (!_visibleWindow.IsDefault)
+            if (_visibleWindow.Width != 0 || _visibleWindow.Height != 0)
             {
                 // We invalidate measure instead of just invalidating arrange because
                 // we don't invalidate measure in UpdateViewport if the view is changing to

+ 5 - 7
src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs

@@ -87,12 +87,10 @@ namespace Avalonia.Controls
         /// Identifies the <see cref="Text" /> property.
         /// </summary>
         /// <value>The identifier for the <see cref="Text" /> property.</value>
-        public static readonly DirectProperty<AutoCompleteBox, string?> TextProperty =
-            TextBlock.TextProperty.AddOwnerWithDataValidation<AutoCompleteBox>(
-                o => o.Text,
-                (o, v) => o.Text = v,
+        public static readonly StyledProperty<string?> TextProperty =
+            TextBlock.TextProperty.AddOwner<AutoCompleteBox>(new(string.Empty,
                 defaultBindingMode: BindingMode.TwoWay,
-                enableDataValidation: true);
+                enableDataValidation: true));
 
         /// <summary>
         /// Identifies the <see cref="SearchText" /> property.
@@ -317,8 +315,8 @@ namespace Avalonia.Controls
         /// <see cref="AutoCompleteBox" /> control.</value>
         public string? Text
         {
-            get => _text;
-            set => SetAndRaise(TextProperty, ref _text, value);
+            get => GetValue(TextProperty);
+            set => SetValue(TextProperty, value);
         }
 
         /// <summary>

+ 1 - 2
src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs

@@ -198,7 +198,6 @@ namespace Avalonia.Controls
         private bool _isDropDownOpen;
         private bool _isFocused = false;
 
-        private string? _text = string.Empty;
         private string? _searchText = string.Empty;
 
         private AutoCompleteFilterPredicate<object?>? _itemFilter;
@@ -1275,7 +1274,7 @@ namespace Avalonia.Controls
             if ((userInitiated ?? true) && Text != value)
             {
                 _ignoreTextPropertyChange++;
-                Text = value;
+                SetCurrentValue(TextProperty, value);
                 callTextChanged = true;
             }
 

+ 1 - 1
src/Avalonia.Controls/BorderVisual.cs

@@ -50,7 +50,7 @@ class CompositionBorderVisual : CompositionDrawListVisual
             if (ClipToBounds)
             {
                 var clipRect = Root!.SnapToDevicePixels(new Rect(new Size(Size.X, Size.Y)));
-                if (_cornerRadius.IsDefault)
+                if (_cornerRadius == default)
                     canvas.PushClip(clipRect);
                 else
                     canvas.PushClip(new RoundedRect(clipRect, _cornerRadius));

+ 20 - 0
src/Avalonia.Controls/ContainerClearingEventArgs.cs

@@ -0,0 +1,20 @@
+using System;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Provides data for the <see cref="ItemsControl.ContainerClearing"/> event.
+    /// </summary>
+    public class ContainerClearingEventArgs : EventArgs
+    {
+        public ContainerClearingEventArgs(Control container)
+        {
+            Container = container;
+        }
+
+        /// <summary>
+        /// Gets the prepared container.
+        /// </summary>
+        public Control Container { get; }
+    }
+}

+ 32 - 0
src/Avalonia.Controls/ContainerIndexChangedEventArgs.cs

@@ -0,0 +1,32 @@
+using System;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Provides data for the <see cref="ItemsControl.ContainerIndexChanged"/> event.
+    /// </summary>
+    public class ContainerIndexChangedEventArgs : EventArgs
+    {
+        public ContainerIndexChangedEventArgs(Control container, int oldIndex, int newIndex)
+        {
+            Container = container;
+            OldIndex = oldIndex;
+            NewIndex = newIndex;
+        }
+
+        /// <summary>
+        /// Get the container for which the index changed.
+        /// </summary>
+        public Control Container { get; }
+
+        /// <summary>
+        /// Gets the index of the container after the change.
+        /// </summary>
+        public int NewIndex { get; }
+
+        /// <summary>
+        /// Gets the index of the container before the change.
+        /// </summary>
+        public int OldIndex { get; }
+    }
+}

+ 26 - 0
src/Avalonia.Controls/ContainerPreparedEventArgs.cs

@@ -0,0 +1,26 @@
+using System;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Provides data for the <see cref="ItemsControl.ContainerPrepared"/> event.
+    /// </summary>
+    public class ContainerPreparedEventArgs : EventArgs
+    {
+        public ContainerPreparedEventArgs(Control container, int index)
+        {
+            Container = container;
+            Index = index;
+        }
+
+        /// <summary>
+        /// Gets the prepared container.
+        /// </summary>
+        public Control Container { get; }
+
+        /// <summary>
+        /// Gets the index of the item the container was prepared for.
+        /// </summary>
+        public int Index { get; }
+    }
+}

+ 45 - 58
src/Avalonia.Controls/ContextMenu.cs

@@ -54,11 +54,17 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
             Popup.PlacementGravityProperty.AddOwner<ContextMenu>();
 
+        /// <summary>
+        /// Defines the <see cref="Placement"/> property.
+        /// </summary>
+        public static readonly StyledProperty<PlacementMode> PlacementProperty =
+            Popup.PlacementProperty.AddOwner<ContextMenu>();
+
         /// <summary>
         /// Defines the <see cref="PlacementMode"/> property.
         /// </summary>
-        public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
-            Popup.PlacementModeProperty.AddOwner<ContextMenu>();
+        [Obsolete("Use the Placement property instead.")]
+        public static readonly StyledProperty<PlacementMode> PlacementModeProperty = PlacementProperty;
 
         /// <summary>
         /// Defines the <see cref="PlacementRect"/> property.
@@ -108,99 +114,80 @@ namespace Avalonia.Controls
         static ContextMenu()
         {
             ItemsPanelProperty.OverrideDefaultValue<ContextMenu>(DefaultPanel);
-            PlacementModeProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer);
+            PlacementProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer);
             ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
             AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<ContextMenu>(AccessibilityView.Control);
             AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<ContextMenu>(AutomationControlType.Menu);
         }
 
-        /// <summary>
-        /// Gets or sets the Horizontal offset of the context menu in relation to the <see cref="PlacementTarget"/>.
-        /// </summary>
+        /// <inheritdoc cref="Popup.HorizontalOffset"/>
         public double HorizontalOffset
         {
-            get { return GetValue(HorizontalOffsetProperty); }
-            set { SetValue(HorizontalOffsetProperty, value); }
+            get => GetValue(HorizontalOffsetProperty);
+            set => SetValue(HorizontalOffsetProperty, value);
         }
 
-        /// <summary>
-        /// Gets or sets the Vertical offset of the context menu in relation to the <see cref="PlacementTarget"/>.
-        /// </summary>
+        /// <inheritdoc cref="Popup.VerticalOffset"/>
         public double VerticalOffset
         {
-            get { return GetValue(VerticalOffsetProperty); }
-            set { SetValue(VerticalOffsetProperty, value); }
+            get => GetValue(VerticalOffsetProperty);
+            set => SetValue(VerticalOffsetProperty, value);
         }
 
-        /// <summary>
-        /// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="PlacementMode"/>
-        /// is <see cref="PlacementMode.AnchorAndGravity"/>.
-        /// </summary>
+        /// <inheritdoc cref="Popup.PlacementAnchor"/>
         public PopupAnchor PlacementAnchor
         {
-            get { return GetValue(PlacementAnchorProperty); }
-            set { SetValue(PlacementAnchorProperty, value); }
+            get => GetValue(PlacementAnchorProperty);
+            set => SetValue(PlacementAnchorProperty, value);
         }
 
-        /// <summary>
-        /// Gets or sets a value describing how the context menu position will be adjusted if the
-        /// unadjusted position would result in the context menu being partly constrained.
-        /// </summary>
+        /// <inheritdoc cref="Popup.PlacementConstraintAdjustment"/>
         public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
         {
-            get { return GetValue(PlacementConstraintAdjustmentProperty); }
-            set { SetValue(PlacementConstraintAdjustmentProperty, value); }
+            get => GetValue(PlacementConstraintAdjustmentProperty);
+            set => SetValue(PlacementConstraintAdjustmentProperty, value);
         }
 
-        /// <summary>
-        /// Gets or sets a value which defines in what direction the context menu should open
-        /// when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
-        /// </summary>
+        /// <inheritdoc cref="Popup.PlacementGravity"/>
         public PopupGravity PlacementGravity
         {
-            get { return GetValue(PlacementGravityProperty); }
-            set { SetValue(PlacementGravityProperty, value); }
+            get => GetValue(PlacementGravityProperty);
+            set => SetValue(PlacementGravityProperty, value);
         }
 
-        /// <summary>
-        /// Gets or sets the placement mode of the context menu in relation to the<see cref="PlacementTarget"/>.
-        /// </summary>
+        /// <inheritdoc cref="Placement"/>
+        [Obsolete("Use the Placement property instead.")]
         public PlacementMode PlacementMode
         {
-            get { return GetValue(PlacementModeProperty); }
-            set { SetValue(PlacementModeProperty, value); }
+            get => GetValue(PlacementProperty);
+            set => SetValue(PlacementProperty, value);
+        }
+
+        /// <inheritdoc cref="Popup.Placement"/>
+        public PlacementMode Placement
+        {
+            get => GetValue(PlacementProperty);
+            set => SetValue(PlacementProperty, value);
         }
 
         public bool WindowManagerAddShadowHint
         {
-            get { return GetValue(WindowManagerAddShadowHintProperty); }
-            set { SetValue(WindowManagerAddShadowHintProperty, value); }
+            get => GetValue(WindowManagerAddShadowHintProperty);
+            set => SetValue(WindowManagerAddShadowHintProperty, value);
         }
 
-        /// <summary>
-        /// Gets or sets the the anchor rectangle within the parent that the context menu will be placed
-        /// relative to when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
-        /// </summary>
-        /// <remarks>
-        /// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around
-        /// which the popup will be opened, with <see cref="PlacementAnchor"/> determining which edge
-        /// of the placement target is used.
-        /// 
-        /// If unset, the anchor rectangle will be the bounds of the <see cref="PlacementTarget"/>.
-        /// </remarks>
+        /// <inheritdoc cref="Popup.PlacementRect"/>
         public Rect? PlacementRect
         {
-            get { return GetValue(PlacementRectProperty); }
-            set { SetValue(PlacementRectProperty, value); }
+            get => GetValue(PlacementRectProperty);
+            set => SetValue(PlacementRectProperty, value);
         }
 
-        /// <summary>
-        /// Gets or sets the control that is used to determine the popup's position.
-        /// </summary>
+        /// <inheritdoc cref="Popup.PlacementTarget"/>
         public Control? PlacementTarget
         {
-            get { return GetValue(PlacementTargetProperty); }
-            set { SetValue(PlacementTargetProperty, value); }
+            get => GetValue(PlacementTargetProperty);
+            set => SetValue(PlacementTargetProperty, value);
         }
 
         /// <summary>
@@ -343,9 +330,9 @@ namespace Avalonia.Controls
                 ((ISetLogicalParent)_popup).SetParent(control);
             }
 
-            _popup.PlacementMode = !requestedByPointer && PlacementMode == PlacementMode.Pointer
+            _popup.Placement = !requestedByPointer && Placement == PlacementMode.Pointer
                 ? PlacementMode.Bottom
-                : PlacementMode;
+                : Placement;
 
             //Position of the line below is really important. 
             //All styles are being applied only when control has logical parent.

+ 1 - 1
src/Avalonia.Controls/DateTimePickers/DatePicker.cs

@@ -389,7 +389,7 @@ namespace Avalonia.Controls
 
             _presenter.Date = SelectedDate ?? DateTimeOffset.Now;
 
-            _popup.PlacementMode = PlacementMode.AnchorAndGravity;
+            _popup.Placement = PlacementMode.AnchorAndGravity;
             _popup.PlacementAnchor = Primitives.PopupPositioning.PopupAnchor.Bottom;
             _popup.PlacementGravity = Primitives.PopupPositioning.PopupGravity.Bottom;
             _popup.PlacementConstraintAdjustment = Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;

+ 1 - 1
src/Avalonia.Controls/DateTimePickers/TimePicker.cs

@@ -255,7 +255,7 @@ namespace Avalonia.Controls
 
             _presenter.Time = SelectedTime ?? DateTime.Now.TimeOfDay;
 
-            _popup.PlacementMode = PlacementMode.AnchorAndGravity;
+            _popup.Placement = PlacementMode.AnchorAndGravity;
             _popup.PlacementAnchor = Primitives.PopupPositioning.PopupAnchor.Bottom;
             _popup.PlacementGravity = Primitives.PopupPositioning.PopupGravity.Bottom;
             _popup.PlacementConstraintAdjustment = Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;

+ 4 - 4
src/Avalonia.Controls/Documents/InlineCollection.cs

@@ -91,11 +91,11 @@ namespace Avalonia.Controls.Documents
 
         public override void Add(Inline inline)
         {
-            if (InlineHost is TextBlock textBlock && !string.IsNullOrEmpty(textBlock._text))
+            if (InlineHost is TextBlock textBlock && !string.IsNullOrEmpty(textBlock.Text))
             {
-                base.Add(new Run(textBlock._text));
+                base.Add(new Run(textBlock.Text));
 
-                textBlock._text = null;
+                textBlock.ClearTextInternal();
             }
 
             base.Add(inline);
@@ -113,7 +113,7 @@ namespace Avalonia.Controls.Documents
         {
             if (InlineHost is TextBlock textBlock && !textBlock.HasComplexContent)
             {
-                textBlock._text += text;
+                textBlock.Text += text;
             }
             else
             {

+ 3 - 1
src/Avalonia.Controls/Flyouts/MenuFlyout.cs

@@ -19,7 +19,9 @@ namespace Avalonia.Controls
         /// Defines the <see cref="Items"/> property
         /// </summary>
         public static readonly DirectProperty<MenuFlyout, IEnumerable?> ItemsProperty =
-            ItemsControl.ItemsProperty.AddOwner<MenuFlyout>(x => x.Items,
+            AvaloniaProperty.RegisterDirect<MenuFlyout, IEnumerable?>(
+                nameof(Items),
+                x => x.Items,
                 (x, v) => x.Items = v);
 
         /// <summary>

+ 7 - 9
src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs

@@ -14,9 +14,9 @@ namespace Avalonia.Controls.Primitives
 {
     public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
     {
-        /// <inheritdoc cref="Popup.PlacementModeProperty"/>
+        /// <inheritdoc cref="Popup.PlacementProperty"/>
         public static readonly StyledProperty<PlacementMode> PlacementProperty =
-            Popup.PlacementModeProperty.AddOwner<PopupFlyoutBase>();
+            Popup.PlacementProperty.AddOwner<PopupFlyoutBase>();
 
         /// <inheritdoc cref="Popup.HorizontalOffsetProperty"/>
         public static readonly StyledProperty<double> HorizontalOffsetProperty =
@@ -64,15 +64,13 @@ namespace Avalonia.Controls.Primitives
 
         protected Popup Popup => _popupLazy.Value;
 
-        /// <summary>
-        /// Gets or sets the desired placement.
-        /// </summary>
+        /// <inheritdoc cref="Popup.Placement"/>
         public PlacementMode Placement
         {
             get => GetValue(PlacementProperty);
             set => SetValue(PlacementProperty, value);
         }
-        
+
         /// <inheritdoc cref="Popup.PlacementGravity"/>
         public PopupGravity PlacementGravity
         {
@@ -407,7 +405,7 @@ namespace Avalonia.Controls.Primitives
         {
             Size sz;
             // Popup.Child can't be null here, it was set in ShowAtCore.
-            if (Popup.Child!.DesiredSize.IsDefault)
+            if (Popup.Child!.DesiredSize == default)
             {
                 // Popup may not have been shown yet. Measure content
                 sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
@@ -423,11 +421,11 @@ namespace Avalonia.Controls.Primitives
             Popup.PlacementGravity = PlacementGravity;
             if (showAtPointer)
             {
-                Popup.PlacementMode = PlacementMode.Pointer;
+                Popup.Placement = PlacementMode.Pointer;
             }
             else
             {
-                Popup.PlacementMode = Placement;
+                Popup.Placement = Placement;
                 Popup.PlacementConstraintAdjustment =
                     PopupPositioning.PopupPositionerConstraintAdjustment.SlideX |
                     PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;

+ 165 - 0
src/Avalonia.Controls/ItemCollection.cs

@@ -0,0 +1,165 @@
+using System;
+using System.Collections;
+using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Collections;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Holds the list of items that constitute the content of an <see cref="ItemsControl"/>.
+    /// </summary>
+    public class ItemCollection : ItemsSourceView, IList
+    {
+// Suppress "Avoid zero-length array allocations": This is a sentinel value and must be unique.
+#pragma warning disable CA1825
+        private static readonly object?[] s_uninitialized = new object?[0];
+#pragma warning restore CA1825
+
+        private Mode _mode;
+
+        internal ItemCollection()
+            : base(s_uninitialized)
+        {
+        }
+
+        public new object? this[int index]
+        {
+            get => base[index];
+            set => WritableSource[index] = value;
+        }
+
+        public bool IsReadOnly => _mode == Mode.ItemsSource;
+
+        internal event EventHandler? SourceChanged;
+
+        /// <summary>
+        /// Adds an item to the <see cref="ItemsControl"/>.
+        /// </summary>
+        /// <param name="value">The item to add to the collection.</param>
+        /// <returns>
+        /// The position into which the new element was inserted, or -1 to indicate that
+        /// the item was not inserted into the collection.
+        /// </returns>
+        /// <exception cref="InvalidOperationException">
+        /// The collection is in ItemsSource mode.
+        /// </exception>
+        public int Add(object? value) => WritableSource.Add(value);
+
+        /// <summary>
+        /// Clears the collection and releases the references on all items currently in the
+        /// collection.
+        /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The collection is in ItemsSource mode.
+        /// </exception>
+        public void Clear() => WritableSource.Clear();
+
+        /// <summary>
+        /// Inserts an element into the collection at the specified index.
+        /// </summary>
+        /// <param name="index">The zero-based index at which to insert the item.</param>
+        /// <param name="value">The item to insert.</param>
+        /// <exception cref="InvalidOperationException">
+        /// The collection is in ItemsSource mode.
+        /// </exception>
+        public void Insert(int index, object? value) => WritableSource.Insert(index, value);
+
+        /// <summary>
+        /// Removes the item at the specified index of the collection or view.
+        /// </summary>
+        /// <param name="index">The zero-based index of the item to remove.</param>
+        /// <exception cref="InvalidOperationException">
+        /// The collection is in ItemsSource mode.
+        /// </exception>
+        public void RemoveAt(int index) => WritableSource.RemoveAt(index);
+
+        /// <summary>
+        /// Removes the specified item reference from the collection or view.
+        /// </summary>
+        /// <param name="value">The object to remove.</param>
+        /// <returns>True if the item was removed; otherwise false.</returns>
+        /// <exception cref="InvalidOperationException">
+        /// The collection is in ItemsSource mode.
+        /// </exception>
+        public bool Remove(object? value)
+        {
+            var c = Count;
+            WritableSource.Remove(value);
+            return Count < c;
+        }
+
+        int IList.Add(object? value) => Add(value);
+        void IList.Clear() => Clear();
+        void IList.Insert(int index, object? value) => Insert(index, value);
+        void IList.RemoveAt(int index) => RemoveAt(index);
+
+        private IList WritableSource
+        {
+            get
+            {
+                if (IsReadOnly)
+                    ThrowIsItemsSource();
+                if (Source == s_uninitialized)
+                    SetSource(CreateDefaultCollection());
+                return Source;
+            }
+        }
+
+        internal IList? GetItemsPropertyValue()
+        {
+            if (_mode == Mode.ObsoleteItemsSetter)
+                return Source == s_uninitialized ? null : Source;
+            return this;
+        }
+
+        internal void SetItems(IList? items)
+        {
+            _mode = Mode.ObsoleteItemsSetter;
+            SetSource(items ?? s_uninitialized);
+        }
+
+        internal void SetItemsSource(IEnumerable? value)
+        {
+            if (_mode != Mode.ItemsSource && Count > 0)
+                throw new InvalidOperationException(
+                    "Items collection must be empty before using ItemsSource.");
+
+            _mode = value is not null ? Mode.ItemsSource : Mode.Items;
+            SetSource(value ?? CreateDefaultCollection());
+        }
+
+        private new void SetSource(IEnumerable source)
+        {
+            var oldSource = Source;
+
+            base.SetSource(source);
+
+            if (oldSource.Count > 0)
+                RaiseCollectionChanged(new(NotifyCollectionChangedAction.Remove, oldSource, 0));
+            if (Source.Count > 0)
+                RaiseCollectionChanged(new(NotifyCollectionChangedAction.Add, Source, 0));
+            SourceChanged?.Invoke(this, EventArgs.Empty);
+        }
+
+        private static AvaloniaList<object?> CreateDefaultCollection()
+        {
+            return new() { ResetBehavior = ResetBehavior.Remove };
+        }
+
+        [DoesNotReturn]
+        private static void ThrowIsItemsSource()
+        {
+            throw new InvalidOperationException(
+                "Operation is not valid while ItemsSource is in use." +
+                "Access and modify elements with ItemsControl.ItemsSource instead.");
+        }
+
+        private enum Mode
+        {
+            Items,
+            ItemsSource,
+            ObsoleteItemsSetter,
+        }
+    }
+}

+ 160 - 83
src/Avalonia.Controls/ItemsControl.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
 using Avalonia.Automation.Peers;
 using Avalonia.Collections;
 using Avalonia.Controls.Generators;
@@ -34,8 +35,13 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="Items"/> property.
         /// </summary>
-        public static readonly DirectProperty<ItemsControl, IEnumerable?> ItemsProperty =
-            AvaloniaProperty.RegisterDirect<ItemsControl, IEnumerable?>(nameof(Items), o => o.Items, (o, v) => o.Items = v);
+        public static readonly DirectProperty<ItemsControl, IList?> ItemsProperty =
+            AvaloniaProperty.RegisterDirect<ItemsControl, IList?>(
+                nameof(Items),
+                o => o.Items,
+#pragma warning disable CS0618 // Type or member is obsolete
+                (o, v) => o.Items = v);
+#pragma warning restore CS0618 // Type or member is obsolete
 
         /// <summary>
         /// Defines the <see cref="ItemContainerTheme"/> property.
@@ -56,23 +62,23 @@ namespace Avalonia.Controls
             AvaloniaProperty.Register<ItemsControl, ITemplate<Panel>>(nameof(ItemsPanel), DefaultPanel);
 
         /// <summary>
-        /// Defines the <see cref="ItemTemplate"/> property.
+        /// Defines the <see cref="ItemsSource"/> property.
         /// </summary>
-        public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
-            AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate));
+        public static readonly StyledProperty<IEnumerable?> ItemsSourceProperty =
+            AvaloniaProperty.Register<ItemsControl, IEnumerable?>(nameof(ItemsSource));
 
         /// <summary>
-        /// Defines the <see cref="ItemsView"/> property.
+        /// Defines the <see cref="ItemTemplate"/> property.
         /// </summary>
-        public static readonly DirectProperty<ItemsControl, ItemsSourceView> ItemsViewProperty =
-            AvaloniaProperty.RegisterDirect<ItemsControl, ItemsSourceView>(nameof(ItemsView), o => o.ItemsView);
+        public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
+            AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate));
 
         /// <summary>
         /// Defines the <see cref="DisplayMemberBinding" /> property
         /// </summary>
         public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
             AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding));
-        
+
         /// <summary>
         /// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
         /// </summary>
@@ -89,15 +95,15 @@ namespace Avalonia.Controls
         /// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
         /// </summary>
         [AssignBinding]
+        [InheritDataTypeFromItems(nameof(ItemsSource))]
         [InheritDataTypeFromItems(nameof(Items))]
         public IBinding? DisplayMemberBinding
         {
             get => GetValue(DisplayMemberBindingProperty);
             set => SetValue(DisplayMemberBindingProperty, value);
         }
-        
-        private IEnumerable? _items = new AvaloniaList<object>();
-        private ItemsSourceView _itemsView;
+
+        private readonly ItemCollection _items = new();
         private int _itemCount;
         private ItemContainerGenerator? _itemContainerGenerator;
         private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
@@ -110,9 +116,8 @@ namespace Avalonia.Controls
         /// </summary>
         public ItemsControl()
         {
-            _itemsView = ItemsSourceView.GetOrCreate(_items);
-            _itemsView.PostCollectionChanged += ItemsCollectionChanged;
-            UpdatePseudoClasses(0);
+            UpdatePseudoClasses();
+            _items.CollectionChanged += OnItemsViewCollectionChanged;
         }
 
         /// <summary>
@@ -128,11 +133,45 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets or sets the items to display.
         /// </summary>
+        /// <remarks>
+        /// Since Avalonia 11, <see cref="ItemsControl"/> has both an <see cref="Items"/> property
+        /// and an <see cref="ItemsSource"/> property. The properties have the following differences:
+        /// 
+        /// <list type="bullet">
+        /// <item><see cref="Items"/> is initialized with an empty collection and is a direct property,
+        /// meaning that it cannot be styled </item>
+        /// <item><see cref="ItemsSource"/> is by default null, and is a styled property. This property
+        /// is marked as the content property and will be used for items added via inline XAML.</item>
+        /// </list>
+        /// 
+        /// In Avalonia 11 the two properties can be used almost interchangeably but this will change
+        /// in a later version. In order to be ready for this change, follow the following guidance:
+        /// 
+        /// <list type="bullet">
+        /// <item>You should use the <see cref="Items"/> property when you're assigning a collection of
+        /// item containers directly, for example adding a collection of <see cref="ListBoxItem"/>s
+        /// directly to a <see cref="ListBox"/>. Add the containers to the pre-existing list, do not
+        /// reassign the <see cref="Items"/> property via the setter or with a binding.</item>
+        /// <item>You should use the <see cref="ItemsSource"/> property when you're assigning or
+        /// binding a collection of models which will be transformed by a data template.</item>
+        /// </list>
+        /// </remarks>
         [Content]
-        public IEnumerable? Items
+        public IList? Items
         {
-            get => _items;
-            set => SetAndRaise(ItemsProperty, ref _items, value);
+            get => _items.GetItemsPropertyValue();
+
+            [Obsolete("Use ItemsSource to set or bind items.")]
+            set
+            {
+                var oldItems = _items.GetItemsPropertyValue();
+
+                if (value != oldItems)
+                {
+                    _items.SetItems(value);
+                    RaisePropertyChanged(ItemsProperty, oldItems, value);
+                }
+            }
         }
 
         /// <summary>
@@ -140,17 +179,24 @@ namespace Avalonia.Controls
         /// </summary>
         public ControlTheme? ItemContainerTheme
         {
-            get => GetValue(ItemContainerThemeProperty); 
+            get => GetValue(ItemContainerThemeProperty);
             set => SetValue(ItemContainerThemeProperty, value);
         }
 
         /// <summary>
-        /// Gets the number of items in <see cref="Items"/>.
+        /// Gets the number of items being displayed by the <see cref="ItemsControl"/>.
         /// </summary>
         public int ItemCount
         {
             get => _itemCount;
-            private set => SetAndRaise(ItemCountProperty, ref _itemCount, value);
+            private set
+            {
+                if (SetAndRaise(ItemCountProperty, ref _itemCount, value))
+                {
+                    UpdatePseudoClasses();
+                    _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged);
+                }
+            }
         }
 
         /// <summary>
@@ -162,13 +208,46 @@ namespace Avalonia.Controls
             set => SetValue(ItemsPanelProperty, value);
         }
 
+        /// <summary>
+        /// Gets or sets a collection used to generate the content of the <see cref="ItemsControl"/>.
+        /// </summary>
+        /// <remarks>
+        /// Since Avalonia 11, <see cref="ItemsControl"/> has both an <see cref="Items"/> property
+        /// and an <see cref="ItemsSource"/> property. The properties have the following differences:
+        /// 
+        /// <list type="bullet">
+        /// <item><see cref="Items"/> is initialized with an empty collection and is a direct property,
+        /// meaning that it cannot be styled </item>
+        /// <item><see cref="ItemsSource"/> is by default null, and is a styled property. This property
+        /// is marked as the content property and will be used for items added via inline XAML.</item>
+        /// </list>
+        /// 
+        /// In Avalonia 11 the two properties can be used almost interchangeably but this will change
+        /// in a later version. In order to be ready for this change, follow the following guidance:
+        /// 
+        /// <list type="bullet">
+        /// <item>You should use the <see cref="Items"/> property when you're assigning a collection of
+        /// item containers directly, for example adding a collection of <see cref="ListBoxItem"/>s
+        /// directly to a <see cref="ListBox"/>. Add the containers to the pre-existing list, do not
+        /// reassign the <see cref="Items"/> property via the setter or with a binding.</item>
+        /// <item>You should use the <see cref="ItemsSource"/> property when you're assigning or
+        /// binding a collection of models which will be transformed by a data template.</item>
+        /// </list>
+        /// </remarks>
+        public IEnumerable? ItemsSource
+        {
+            get => GetValue(ItemsSourceProperty);
+            set => SetValue(ItemsSourceProperty, value);
+        }
+
         /// <summary>
         /// Gets or sets the data template used to display the items in the control.
         /// </summary>
+        [InheritDataTypeFromItems(nameof(ItemsSource))]
         [InheritDataTypeFromItems(nameof(Items))]
         public IDataTemplate? ItemTemplate
         {
-            get => GetValue(ItemTemplateProperty); 
+            get => GetValue(ItemTemplateProperty);
             set => SetValue(ItemTemplateProperty, value);
         }
 
@@ -183,31 +262,9 @@ namespace Avalonia.Controls
         public Panel? ItemsPanelRoot => Presenter?.Panel;
 
         /// <summary>
-        /// Gets a standardized view over <see cref="Items"/>.
+        /// Gets a read-only view of the items in the <see cref="ItemsControl"/>.
         /// </summary>
-        /// <remarks>
-        /// The <see cref="Items"/> property may be an enumerable which does not implement
-        /// <see cref="IList"/> or may be null. This view can be used to provide a standardized
-        /// view of the current items regardless of the type of the concrete collection, and
-        /// without having to deal with null values.
-        /// </remarks>
-        public ItemsSourceView ItemsView 
-        {
-            get => _itemsView;
-            private set
-            {
-                if (ReferenceEquals(_itemsView, value))
-                    return;
-
-                var oldValue = _itemsView;
-                RemoveControlItemsFromLogicalChildren(_itemsView);
-                _itemsView.PostCollectionChanged -= ItemsCollectionChanged;
-                _itemsView = value;
-                _itemsView.PostCollectionChanged += ItemsCollectionChanged;
-                AddControlItemsToLogicalChildren(_itemsView);
-                RaisePropertyChanged(ItemsViewProperty, oldValue, _itemsView);
-            }
-        }
+        public ItemsSourceView ItemsView => _items;
 
         private protected bool WrapFocus { get; set; }
 
@@ -217,6 +274,34 @@ namespace Avalonia.Controls
             remove => _childIndexChanged -= value;
         }
 
+        /// <summary>
+        /// Occurs each time a container is prepared for use.
+        /// </summary>
+        /// <remarks>
+        /// The prepared element might be newly created or an existing container that is being re-
+        /// used.
+        /// </remarks>
+        public event EventHandler<ContainerPreparedEventArgs>? ContainerPrepared;
+
+        /// <summary>
+        /// Occurs for each realized container when the index for the item it represents has changed.
+        /// </summary>
+        /// <remarks>
+        /// This event is raised for each realized container 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<ContainerIndexChangedEventArgs>? ContainerIndexChanged;
+
+        /// <summary>
+        /// Occurs each time a container is cleared.
+        /// </summary>
+        /// <remarks>
+        /// This event is raised immediately each time an container is cleared, such as when it
+        /// falls outside the range of realized items or the corresponding item is removed.
+        /// </remarks>
+        public event EventHandler<ContainerClearingEventArgs>? ContainerClearing;
+
         /// <inheritdoc />
         public event EventHandler<RoutedEventArgs> HorizontalSnapPointsChanged
         {
@@ -262,7 +347,7 @@ namespace Avalonia.Controls
         /// </summary>
         public bool AreHorizontalSnapPointsRegular
         {
-            get => GetValue(AreHorizontalSnapPointsRegularProperty); 
+            get => GetValue(AreHorizontalSnapPointsRegularProperty);
             set => SetValue(AreHorizontalSnapPointsRegularProperty, value);
         }
 
@@ -271,7 +356,7 @@ namespace Avalonia.Controls
         /// </summary>
         public bool AreVerticalSnapPointsRegular
         {
-            get => GetValue(AreVerticalSnapPointsRegularProperty); 
+            get => GetValue(AreVerticalSnapPointsRegularProperty);
             set => SetValue(AreVerticalSnapPointsRegularProperty, value);
         }
 
@@ -295,7 +380,7 @@ namespace Avalonia.Controls
         /// </returns>
         public Control? ContainerFromItem(object item)
         {
-            var index = ItemsView.IndexOf(item);
+            var index = _items.IndexOf(item);
             return index >= 0 ? ContainerFromIndex(index) : null;
         }
 
@@ -319,7 +404,7 @@ namespace Avalonia.Controls
         public object? ItemFromContainer(Control container)
         {
             var index = IndexFromContainer(container);
-            return index >= 0 && index < ItemsView.Count ? ItemsView[index] : null;
+            return index >= 0 && index < _items.Count ? _items[index] : null;
         }
 
         /// <summary>
@@ -478,19 +563,13 @@ namespace Avalonia.Controls
         {
             base.OnPropertyChanged(change);
 
-            if (change.Property == ItemsProperty)
-            {
-                ItemsView = ItemsSourceView.GetOrCreate(change.GetNewValue<IEnumerable?>());
-                ItemCount = ItemsView.Count;
-            }
-            else if (change.Property == ItemCountProperty)
+            if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
             {
-                UpdatePseudoClasses(change.GetNewValue<int>());
-                _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged);
+                RefreshContainers();
             }
-            else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
+            else if (change.Property == ItemsSourceProperty)
             {
-                RefreshContainers();
+                _items.SetItemsSource(change.GetNewValue<IEnumerable?>());
             }
             else if (change.Property == ItemTemplateProperty)
             {
@@ -517,24 +596,27 @@ namespace Avalonia.Controls
 
         /// <summary>
         /// Called when the <see cref="INotifyCollectionChanged.CollectionChanged"/> event is
-        /// raised on <see cref="Items"/>.
+        /// raised on <see cref="ItemsView"/>.
         /// </summary>
         /// <param name="sender">The event sender.</param>
         /// <param name="e">The event args.</param>
-        protected virtual void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+        private protected virtual void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
         {
-            ItemCount = _itemsView.Count;
-
-            switch (e.Action)
+            if (!_items.IsReadOnly)
             {
-                case NotifyCollectionChangedAction.Add:
-                    AddControlItemsToLogicalChildren(e.NewItems);
-                    break;
+                switch (e.Action)
+                {
+                    case NotifyCollectionChangedAction.Add:
+                        AddControlItemsToLogicalChildren(e.NewItems);
+                        break;
 
-                case NotifyCollectionChangedAction.Remove:
-                    RemoveControlItemsFromLogicalChildren(e.OldItems);
-                    break;
+                    case NotifyCollectionChangedAction.Remove:
+                        RemoveControlItemsFromLogicalChildren(e.OldItems);
+                        break;
+                }
             }
+
+            ItemCount = ItemsView.Count;
         }
 
         /// <summary>
@@ -578,7 +660,7 @@ namespace Avalonia.Controls
         {
             var itemContainerTheme = ItemContainerTheme;
 
-            if (itemContainerTheme is not null && 
+            if (itemContainerTheme is not null &&
                 !container.IsSet(ThemeProperty) &&
                 ((IStyleable)container).StyleKey == itemContainerTheme.TargetType)
             {
@@ -595,24 +677,23 @@ namespace Avalonia.Controls
         {
             _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index));
             _scrollViewer?.RegisterAnchorCandidate(container);
+            ContainerPrepared?.Invoke(this, new(container, index));
         }
 
         internal void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex)
         {
             ContainerIndexChangedOverride(container, oldIndex, newIndex);
             _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, newIndex));
+            ContainerIndexChanged?.Invoke(this, new(container, oldIndex, newIndex));
         }
 
         internal void ClearItemContainer(Control container)
         {
             _scrollViewer?.UnregisterAnchorCandidate(container);
             ClearContainerForItemOverride(container);
+            ContainerClearing?.Invoke(this, new(container));
         }
 
-        /// <summary>
-        /// Given a collection of items, adds those that are controls to the logical children.
-        /// </summary>
-        /// <param name="items">The items.</param>
         private void AddControlItemsToLogicalChildren(IEnumerable? items)
         {
             if (items is null)
@@ -633,10 +714,6 @@ namespace Avalonia.Controls
                 LogicalChildren.AddRange(toAdd);
         }
 
-        /// <summary>
-        /// Given a collection of items, removes those that are controls to from logical children.
-        /// </summary>
-        /// <param name="items">The items.</param>
         private void RemoveControlItemsFromLogicalChildren(IEnumerable? items)
         {
             if (items is null)
@@ -674,10 +751,10 @@ namespace Avalonia.Controls
             return _displayMemberItemTemplate;
         }
 
-        private void UpdatePseudoClasses(int itemCount)
+        private void UpdatePseudoClasses()
         {
-            PseudoClasses.Set(":empty", itemCount == 0);
-            PseudoClasses.Set(":singleitem", itemCount == 1);
+            PseudoClasses.Set(":empty", ItemCount == 0);
+            PseudoClasses.Set(":singleitem", ItemCount == 1);
         }
 
         protected static IInputElement? GetNextControl(

+ 91 - 64
src/Avalonia.Controls/ItemsSourceView.cs

@@ -7,6 +7,7 @@ using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Avalonia.Controls.Utils;
 
@@ -17,15 +18,16 @@ namespace Avalonia.Controls
     /// and an items control.
     /// </summary>
     public class ItemsSourceView : IReadOnlyList<object?>,
+        IList,
         INotifyCollectionChanged,
         ICollectionChangedListener
     {
         /// <summary>
-        ///  Gets an empty <see cref="ItemsSourceView"/>
+        /// Gets an empty <see cref="ItemsSourceView"/>
         /// </summary>
-        public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object>());
+        public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object?>());
 
-        private readonly IList _inner;
+        private IList _source;
         private NotifyCollectionChangedEventHandler? _collectionChanged;
         private NotifyCollectionChangedEventHandler? _preCollectionChanged;
         private NotifyCollectionChangedEventHandler? _postCollectionChanged;
@@ -35,30 +37,17 @@ namespace Avalonia.Controls
         /// Initializes a new instance of the ItemsSourceView class for the specified data source.
         /// </summary>
         /// <param name="source">The data source.</param>
-        private protected ItemsSourceView(IEnumerable source)
-        {
-            _inner = source switch
-            {
-                ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)),
-                IList list => list,
-                INotifyCollectionChanged => throw new ArgumentException(
-                    "Collection implements INotifyCollectionChanged but not IList.",
-                    nameof(source)),
-                IEnumerable<object> iObj => new List<object>(iObj),
-                null => throw new ArgumentNullException(nameof(source)),
-                _ => new List<object>(source.Cast<object>())
-            };
-        }
+        private protected ItemsSourceView(IEnumerable source) => SetSource(source);
 
         /// <summary>
         /// Gets the number of items in the collection.
         /// </summary>
-        public int Count => Inner.Count;
+        public int Count => Source.Count;
 
         /// <summary>
-        /// Gets the inner collection.
+        /// Gets the source collection.
         /// </summary>
-        public IList Inner => _inner;
+        public IList Source => _source;
 
         /// <summary>
         /// Retrieves the item at the specified index.
@@ -67,12 +56,20 @@ namespace Avalonia.Controls
         /// <returns>The item.</returns>
         public object? this[int index] => GetAt(index);
 
+        bool IList.IsFixedSize => false;
+        bool IList.IsReadOnly => true;
+        bool ICollection.IsSynchronized => false;
+        object ICollection.SyncRoot => this;
+
+        object? IList.this[int index]
+        {
+            get => GetAt(index);
+            set => ThrowReadOnly();
+        }
+
         /// <summary>
-        /// Gets a value that indicates whether the items source can provide a unique key for each item.
-        /// </summary>
-        /// <remarks>
         /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage.
-        /// </remarks>
+        /// </summary>
         internal bool HasKeyIndexMapping => false;
 
         /// <summary>
@@ -131,39 +128,14 @@ namespace Avalonia.Controls
             }
         }
 
-        private void AddListenerIfNecessary()
-        {
-            if (!_listening)
-            {
-                if (_inner is INotifyCollectionChanged incc)
-                    CollectionChangedEventManager.Instance.AddListener(incc, this);
-                _listening = true;
-            }
-        }
-
-        private void RemoveListenerIfNecessary()
-        {
-            if (_listening && _collectionChanged is null && _postCollectionChanged is null)
-            {
-                if (_inner is INotifyCollectionChanged incc)
-                    CollectionChangedEventManager.Instance.RemoveListener(incc, this);
-                _listening = false;
-            }
-        }
-
         /// <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>
-        /// Determines the index of a specific item in the collection.
-        /// </summary>
-        /// <param name="item">The object to locate in the collection.</param>
-        /// <returns>The index of value if found in the list; otherwise, -1.</returns>
-        public int IndexOf(object? item) => Inner.IndexOf(item);
+        public object? GetAt(int index) => Source[index];
+        public bool Contains(object? item) => Source.Contains(item);
+        public int IndexOf(object? item) => Source.IndexOf(item);
 
         /// <summary>
         /// Gets or creates an <see cref="ItemsSourceView"/> for the specified enumerable.
@@ -201,7 +173,8 @@ namespace Avalonia.Controls
         {
             return items switch
             {
-                ItemsSourceView<T> isv => isv,
+                ItemsSourceView<T> isvt => isvt,
+                ItemsSourceView isv => new ItemsSourceView<T>(isv.Source),
                 null => ItemsSourceView<T>.Empty,
                 _ => new ItemsSourceView<T>(items)
             };
@@ -236,7 +209,7 @@ namespace Avalonia.Controls
                     yield return o;
             }
 
-            var inner = Inner;
+            var inner = Source;
 
             return inner switch
             {
@@ -245,7 +218,7 @@ namespace Avalonia.Controls
             };
         }
 
-        IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
+        IEnumerator IEnumerable.GetEnumerator() => Source.GetEnumerator();
 
         void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
         {
@@ -262,15 +235,69 @@ namespace Avalonia.Controls
             _postCollectionChanged?.Invoke(this, e);
         }
 
+        int IList.Add(object? value) => ThrowReadOnly();
+        void IList.Clear() => ThrowReadOnly();
+        void IList.Insert(int index, object? value) => ThrowReadOnly();
+        void IList.Remove(object? value) => ThrowReadOnly();
+        void IList.RemoveAt(int index) => ThrowReadOnly();
+        void ICollection.CopyTo(Array array, int index) => Source.CopyTo(array, index);
+
         /// <summary>
-        /// Retrieves the index of the item that has the specified unique identifier (key).
+        /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage.
         /// </summary>
-        /// <param name="index">The index.</param>
-        /// <returns>The key</returns>
-        /// <remarks>
-        /// TODO: Not yet implemented in Avalonia.
-        /// </remarks>
         internal string KeyFromIndex(int index) => throw new NotImplementedException();
+
+        private protected void RaiseCollectionChanged(NotifyCollectionChangedEventArgs e)
+        {
+            _preCollectionChanged?.Invoke(this, e);
+            _collectionChanged?.Invoke(this, e);
+            _postCollectionChanged?.Invoke(this, e);
+        }
+
+        [MemberNotNull(nameof(_source))]
+        private protected void SetSource(IEnumerable source)
+        {
+            if (_listening && _source is INotifyCollectionChanged inccOld)
+                CollectionChangedEventManager.Instance.RemoveListener(inccOld, this);
+
+            _source = source switch
+            {
+                ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)),
+                IList list => list,
+                INotifyCollectionChanged => throw new ArgumentException(
+                    "Collection implements INotifyCollectionChanged but not IList.",
+                    nameof(source)),
+                IEnumerable<object> iObj => new List<object>(iObj),
+                null => throw new ArgumentNullException(nameof(source)),
+                _ => new List<object>(source.Cast<object>())
+            };
+
+            if (_listening && _source is INotifyCollectionChanged inccNew)
+                CollectionChangedEventManager.Instance.AddListener(inccNew, this);
+        }
+
+        private void AddListenerIfNecessary()
+        {
+            if (!_listening)
+            {
+                if (_source is INotifyCollectionChanged incc)
+                    CollectionChangedEventManager.Instance.AddListener(incc, this);
+                _listening = true;
+            }
+        }
+
+        private void RemoveListenerIfNecessary()
+        {
+            if (_listening && _collectionChanged is null && _postCollectionChanged is null)
+            {
+                if (_source is INotifyCollectionChanged incc)
+                    CollectionChangedEventManager.Instance.RemoveListener(incc, this);
+                _listening = false;
+            }
+        }
+
+        [DoesNotReturn]
+        private static int ThrowReadOnly() => throw new NotSupportedException("Collection is read-only.");
     }
 
     public sealed class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
@@ -306,7 +333,7 @@ namespace Avalonia.Controls
         /// </summary>
         /// <param name="index">The index.</param>
         /// <returns>The item.</returns>
-        public new T GetAt(int index) => (T)Inner[index]!;
+        public new T GetAt(int index) => (T)Source[index]!;
 
         public new IEnumerator<T> GetEnumerator()
         {
@@ -316,7 +343,7 @@ namespace Avalonia.Controls
                     yield return (T)o;
             }
 
-            var inner = Inner;
+            var inner = Source;
 
             return inner switch
             {
@@ -325,6 +352,6 @@ namespace Avalonia.Controls
             };
         }
 
-        IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
+        IEnumerator IEnumerable.GetEnumerator() => Source.GetEnumerator();
     }
 }

+ 2 - 2
src/Avalonia.Controls/LayoutTransformControl.cs

@@ -91,7 +91,7 @@ namespace Avalonia.Controls
             arrangedsize = TransformRoot.Bounds.Size;
 
             // This is the first opportunity under Silverlight to find out the Child's true DesiredSize
-            if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && _childActualSize.IsDefault)
+            if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && _childActualSize == default)
             {
                 //// Unfortunately, all the work so far is invalid because the wrong DesiredSize was used
                 //// Make a note of the actual DesiredSize
@@ -122,7 +122,7 @@ namespace Avalonia.Controls
             }
 
             Size measureSize;
-            if (_childActualSize.IsDefault)
+            if (_childActualSize == default)
             {
                 // Determine the largest size after the transformation
                 measureSize = ComputeLargestTransformedSize(availableSize);

+ 66 - 69
src/Avalonia.Controls/MaskedTextBox.cs

@@ -15,9 +15,8 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<bool> AsciiOnlyProperty =
              AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(AsciiOnly));
 
-        public static readonly DirectProperty<MaskedTextBox, CultureInfo?> CultureProperty =
-             AvaloniaProperty.RegisterDirect<MaskedTextBox, CultureInfo?>(nameof(Culture), o => o.Culture,
-                (o, v) => o.Culture = v, CultureInfo.CurrentCulture);
+        public static readonly StyledProperty<CultureInfo?> CultureProperty =
+             AvaloniaProperty.Register<MaskedTextBox, CultureInfo?>(nameof(Culture), CultureInfo.CurrentCulture);
 
         public static readonly StyledProperty<bool> HidePromptOnLeaveProperty =
              AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(HidePromptOnLeave));
@@ -32,26 +31,49 @@ namespace Avalonia.Controls
              AvaloniaProperty.Register<MaskedTextBox, string?>(nameof(Mask), string.Empty);
 
         public static readonly StyledProperty<char> PromptCharProperty =
-             AvaloniaProperty.Register<MaskedTextBox, char>(nameof(PromptChar), '_');
+             AvaloniaProperty.Register<MaskedTextBox, char>(nameof(PromptChar), '_', coerce: CoercePromptChar);
 
-        public static readonly DirectProperty<MaskedTextBox, bool> ResetOnPromptProperty =
-             AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnPrompt), o => o.ResetOnPrompt, (o, v) => o.ResetOnPrompt = v);
+        public static readonly StyledProperty<bool> ResetOnPromptProperty =
+             AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(ResetOnPrompt), true);
 
-        public static readonly DirectProperty<MaskedTextBox, bool> ResetOnSpaceProperty =
-             AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnSpace), o => o.ResetOnSpace, (o, v) => o.ResetOnSpace = v);
+        public static readonly StyledProperty<bool> ResetOnSpaceProperty =
+             AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(ResetOnSpace), true);
 
-        private CultureInfo? _culture;
+        private bool _ignoreTextChanges;
 
-        private bool _resetOnPrompt = true;
+        static MaskedTextBox()
+        {
+            PasswordCharProperty.OverrideMetadata<MaskedTextBox>(new('\0', coerce: CoercePasswordChar));
+        }
 
-        private bool _ignoreTextChanges;
+        private static char CoercePasswordChar(AvaloniaObject sender, char baseValue)
+        {
+            if (!MaskedTextProvider.IsValidPasswordChar(baseValue))
+            {
+                throw new ArgumentException($"'{baseValue}' is not a valid value for PasswordChar.");
+            }
+            var textbox = (MaskedTextBox)sender;
+            if (textbox.MaskProvider is { } maskProvider && baseValue == maskProvider.PromptChar)
+            {
+                // Prompt and password chars must be different.
+                throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
+            }
 
-        private bool _resetOnSpace = true;
+            return baseValue;
+        }
 
-        static MaskedTextBox()
+        private static char CoercePromptChar(AvaloniaObject sender, char baseValue)
         {
-            PasswordCharProperty
-                .OverrideDefaultValue<MaskedTextBox>('\0');
+            if (!MaskedTextProvider.IsValidInputChar(baseValue))
+            {
+                throw new ArgumentException($"'{baseValue}' is not a valid value for PromptChar.");
+            }
+            if (baseValue == sender.GetValue(PasswordCharProperty))
+            {
+                throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
+            }
+
+            return baseValue;
         }
 
         public MaskedTextBox() { }
@@ -59,6 +81,9 @@ namespace Avalonia.Controls
         /// <summary>
         ///  Constructs the MaskedTextBox with the specified MaskedTextProvider object.
         /// </summary>
+        [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", 
+            "AVP1012:An AvaloniaObject should use SetCurrentValue when assigning its own StyledProperty or AttachedProperty values", 
+            Justification = "These values are being explicitly provided by a constructor parameter.")]
         public MaskedTextBox(MaskedTextProvider maskedTextProvider)
         {
             if (maskedTextProvider == null)
@@ -87,8 +112,8 @@ namespace Avalonia.Controls
         /// </summary>
         public CultureInfo? Culture
         {
-            get => _culture;
-            set => SetAndRaise(CultureProperty, ref _culture, value);
+            get => GetValue(CultureProperty);
+            set => SetValue(CultureProperty, value);
         }
 
         /// <summary>
@@ -131,15 +156,6 @@ namespace Avalonia.Controls
         /// </summary>
         public MaskedTextProvider? MaskProvider { get; private set; }
 
-        /// <summary>
-        /// Gets or sets the character to be displayed in substitute for user input.
-        /// </summary>
-        public new char PasswordChar
-        {
-            get => GetValue(PasswordCharProperty);
-            set => SetValue(PasswordCharProperty, value);
-        }
-
         /// <summary>
         /// Gets or sets the character used to represent the absence of user input in MaskedTextBox.
         /// </summary>
@@ -154,16 +170,8 @@ namespace Avalonia.Controls
         /// </summary>
         public bool ResetOnPrompt
         {
-            get => _resetOnPrompt;
-            set
-            {
-                SetAndRaise(ResetOnPromptProperty, ref _resetOnPrompt, value);
-                if (MaskProvider != null)
-                {
-                    MaskProvider.ResetOnPrompt = value;
-                }
-
-            }
+            get => GetValue(ResetOnPromptProperty);
+            set => SetValue(ResetOnPromptProperty, value);
         }
 
         /// <summary>
@@ -171,16 +179,8 @@ namespace Avalonia.Controls
         /// </summary>
         public bool ResetOnSpace
         {
-            get => _resetOnSpace;
-            set
-            {
-                SetAndRaise(ResetOnSpaceProperty, ref _resetOnSpace, value);
-                if (MaskProvider != null)
-                {
-                    MaskProvider.ResetOnSpace = value;
-                }
-
-            }
+            get => GetValue(ResetOnSpaceProperty);
+            set => SetValue(ResetOnSpaceProperty, value);
         }
 
         Type IStyleable.StyleKey => typeof(TextBox);
@@ -190,7 +190,7 @@ namespace Avalonia.Controls
         {
             if (HidePromptOnLeave == true && MaskProvider != null)
             {
-                Text = MaskProvider.ToDisplayString();
+                SetCurrentValue(TextProperty, MaskProvider.ToDisplayString());
             }
             base.OnGotFocus(e);
         }
@@ -225,11 +225,11 @@ namespace Avalonia.Controls
                     var index = GetNextCharacterPosition(CaretIndex);
                     if (MaskProvider.InsertAt(item, index))
                     {
-                        CaretIndex = ++index;
+                        SetCurrentValue(CaretIndexProperty, ++index);
                     }
                 }
 
-                Text = MaskProvider.ToDisplayString();
+                SetCurrentValue(TextProperty, MaskProvider.ToDisplayString());
                 e.Handled = true;
                 return;
             }
@@ -279,7 +279,7 @@ namespace Avalonia.Controls
         {
             if (HidePromptOnLeave && MaskProvider != null)
             {
-                Text = MaskProvider.ToString(!HidePromptOnLeave, true);
+                SetCurrentValue(TextProperty, MaskProvider.ToString(!HidePromptOnLeave, true));
             }
             base.OnLostFocus(e);
         }
@@ -326,15 +326,6 @@ namespace Avalonia.Controls
             }
             else if (change.Property == PasswordCharProperty)
             {
-                if (!MaskedTextProvider.IsValidPasswordChar(PasswordChar))
-                {
-                    throw new ArgumentException("Specified character value is not allowed for this property.", nameof(PasswordChar));
-                }
-                if (MaskProvider != null && PasswordChar == MaskProvider.PromptChar)
-                {
-                    // Prompt and password chars must be different.
-                    throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
-                }
                 if (MaskProvider != null && MaskProvider.PasswordChar != PasswordChar)
                 {
                     UpdateMaskProvider();
@@ -342,17 +333,23 @@ namespace Avalonia.Controls
             }
             else if (change.Property == PromptCharProperty)
             {
-                if (!MaskedTextProvider.IsValidInputChar(PromptChar))
+                if (MaskProvider != null && MaskProvider.PromptChar != PromptChar)
                 {
-                    throw new ArgumentException("Specified character value is not allowed for this property.");
+                    UpdateMaskProvider();
                 }
-                if (PromptChar == PasswordChar)
+            }
+            else if (change.Property == ResetOnPromptProperty)
+            {
+                if (MaskProvider != null && change.GetNewValue<bool>() is { } newValue)
                 {
-                    throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
+                    MaskProvider.ResetOnPrompt = newValue;
                 }
-                if (MaskProvider != null && MaskProvider.PromptChar != PromptChar)
+            }
+            else if (change.Property == ResetOnSpaceProperty)
+            {
+                if (MaskProvider != null && change.GetNewValue<bool>() is { } newValue)
                 {
-                    UpdateMaskProvider();
+                    MaskProvider.ResetOnSpace = newValue;
                 }
             }
             else if (change.Property == AsciiOnlyProperty && MaskProvider != null && MaskProvider.AsciiOnly != AsciiOnly
@@ -390,7 +387,7 @@ namespace Avalonia.Controls
 
                 if (CaretIndex < Text?.Length)
                 {
-                    CaretIndex = GetNextCharacterPosition(CaretIndex);
+                    SetCurrentValue(CaretIndexProperty, GetNextCharacterPosition(CaretIndex));
 
                     if (MaskProvider.InsertAt(e.Text!, CaretIndex))
                     {
@@ -399,7 +396,7 @@ namespace Avalonia.Controls
                     var nextPos = GetNextCharacterPosition(CaretIndex);
                     if (nextPos != 0 && CaretIndex != Text.Length)
                     {
-                        CaretIndex = nextPos;
+                        SetCurrentValue(CaretIndexProperty, nextPos);
                     }
                 }
 
@@ -434,8 +431,8 @@ namespace Avalonia.Controls
         {
             if (provider != null)
             {
-                Text = provider.ToDisplayString();
-                CaretIndex = position;
+                SetCurrentValue(TextProperty, provider.ToDisplayString());
+                SetCurrentValue(CaretIndexProperty, position);
             }
         }
 

+ 1 - 1
src/Avalonia.Controls/NativeControlHost.cs

@@ -141,7 +141,7 @@ namespace Avalonia.Controls
 
             if (IsEffectivelyVisible && bounds.HasValue)
             {
-                if (bounds.Value.IsDefault)
+                if (bounds.Value.Width == 0 && bounds.Value.Height == 0)
                     return false;
                 _attachment?.ShowInBounds(bounds.Value);
             }

+ 1 - 17
src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs

@@ -22,7 +22,6 @@ namespace Avalonia.Controls.Presenters
             Debug.Assert(presenter.Panel is not null or VirtualizingPanel);
             
             _presenter = presenter;
-            _presenter.ItemsControl.PropertyChanged += OnItemsControlPropertyChanged;
             _presenter.ItemsControl.ItemsView.PostCollectionChanged += OnItemsChanged;
 
             OnItemsChanged(null, CollectionUtils.ResetEventArgs);
@@ -32,9 +31,7 @@ namespace Avalonia.Controls.Presenters
         {
             if (_presenter.ItemsControl is { } itemsControl)
             {
-                itemsControl.PropertyChanged -= OnItemsControlPropertyChanged;
                 itemsControl.ItemsView.PostCollectionChanged -= OnItemsChanged;
-
                 ClearItemsControlLogicalChildren();
             }
 
@@ -43,18 +40,6 @@ namespace Avalonia.Controls.Presenters
 
         internal void Refresh() => OnItemsChanged(null, CollectionUtils.ResetEventArgs);
 
-        private void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
-        {
-            if (e.Property == ItemsControl.ItemsProperty)
-            {
-                if (e.OldValue is INotifyCollectionChanged inccOld)
-                    inccOld.CollectionChanged -= OnItemsChanged;
-                OnItemsChanged(null, CollectionUtils.ResetEventArgs);
-                if (e.NewValue is INotifyCollectionChanged inccNew)
-                    inccNew.CollectionChanged += OnItemsChanged;
-            }
-        }
-
         private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
         {
             if (_presenter.Panel is null || _presenter.ItemsControl is null)
@@ -84,8 +69,7 @@ namespace Avalonia.Controls.Presenters
                     var c = children[index + i];
                     if (!c.IsSet(ItemIsOwnContainerProperty))
                         itemsControl.RemoveLogicalChild(children[i + index]);
-                    else
-                        generator.ClearItemContainer(c);
+                    generator.ClearItemContainer(c);
                 }
 
                 children.RemoveRange(index, count);

+ 31 - 78
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -9,17 +9,13 @@ using Avalonia.VisualTree;
 using Avalonia.Layout;
 using Avalonia.Media.Immutable;
 using Avalonia.Controls.Documents;
-using Avalonia.Input.TextInput;
-using Avalonia.Data;
 
 namespace Avalonia.Controls.Presenters
 {
     public class TextPresenter : Control
     {
-        public static readonly DirectProperty<TextPresenter, int> CaretIndexProperty =
-            TextBox.CaretIndexProperty.AddOwner<TextPresenter>(
-                o => o.CaretIndex,
-                (o, v) => o.CaretIndex = v);
+        public static readonly StyledProperty<int> CaretIndexProperty =
+            TextBox.CaretIndexProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
 
         public static readonly StyledProperty<bool> RevealPasswordProperty =
             AvaloniaProperty.Register<TextPresenter, bool>(nameof(RevealPassword));
@@ -36,33 +32,23 @@ namespace Avalonia.Controls.Presenters
         public static readonly StyledProperty<IBrush?> CaretBrushProperty =
             AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(CaretBrush));
 
-        public static readonly DirectProperty<TextPresenter, int> SelectionStartProperty =
-            TextBox.SelectionStartProperty.AddOwner<TextPresenter>(
-                o => o.SelectionStart,
-                (o, v) => o.SelectionStart = v);
+        public static readonly StyledProperty<int> SelectionStartProperty =
+            TextBox.SelectionStartProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
 
-        public static readonly DirectProperty<TextPresenter, int> SelectionEndProperty =
-            TextBox.SelectionEndProperty.AddOwner<TextPresenter>(
-                o => o.SelectionEnd,
-                (o, v) => o.SelectionEnd = v);
+        public static readonly StyledProperty<int> SelectionEndProperty =
+            TextBox.SelectionEndProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
 
         /// <summary>
         /// Defines the <see cref="Text"/> property.
         /// </summary>
-        public static readonly DirectProperty<TextPresenter, string?> TextProperty =
-            AvaloniaProperty.RegisterDirect<TextPresenter, string?>(
-                nameof(Text),
-                o => o.Text,
-                (o, v) => o.Text = v, defaultBindingMode: BindingMode.OneWay);
+        public static readonly StyledProperty<string?> TextProperty =
+            TextBlock.TextProperty.AddOwner<TextPresenter>(new(string.Empty));
 
         /// <summary>
         /// Defines the <see cref="PreeditText"/> property.
         /// </summary>
-        public static readonly DirectProperty<TextPresenter, string?> PreeditTextProperty =
-            AvaloniaProperty.RegisterDirect<TextPresenter, string?>(
-                nameof(PreeditText),
-                o => o.PreeditText,
-                 (o, v) => o.PreeditText = v);
+        public static readonly StyledProperty<string?> PreeditTextProperty =
+            AvaloniaProperty.Register<TextPresenter, string?>(nameof(PreeditText));
 
         /// <summary>
         /// Defines the <see cref="CompositionRegion"/> property.
@@ -104,18 +90,13 @@ namespace Avalonia.Controls.Presenters
             Border.BackgroundProperty.AddOwner<TextPresenter>();
 
         private readonly DispatcherTimer _caretTimer;
-        private int _caretIndex;
-        private int _selectionStart;
-        private int _selectionEnd;
         private bool _caretBlink;
-        internal string? _text;
         private TextLayout? _textLayout;
         private Size _constraint;
 
         private CharacterHit _lastCharacterHit;
         private Rect _caretBounds;
         private Point _navigationPosition;
-        private string? _preeditText;
         private TextRange? _compositionRegion;
 
         static TextPresenter()
@@ -125,7 +106,6 @@ namespace Avalonia.Controls.Presenters
 
         public TextPresenter()
         {
-            _text = string.Empty;
             _caretTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
             _caretTimer.Tick += CaretTimerTick;
         }
@@ -147,14 +127,14 @@ namespace Avalonia.Controls.Presenters
         [Content]
         public string? Text
         {
-            get => _text;
-            set => SetAndRaise(TextProperty, ref _text, value);
+            get => GetValue(TextProperty);
+            set => SetValue(TextProperty, value);
         }
 
         public string? PreeditText
         {
-            get => _preeditText;
-            set => SetAndRaise(PreeditTextProperty, ref _preeditText, value);
+            get => GetValue(PreeditTextProperty);
+            set => SetValue(PreeditTextProperty, value);
         }
 
         public TextRange? CompositionRegion
@@ -275,17 +255,8 @@ namespace Avalonia.Controls.Presenters
 
         public int CaretIndex
         {
-            get
-            {
-                return _caretIndex;
-            }
-            set
-            {
-                if (value != _caretIndex)
-                {
-                    MoveCaretToTextPosition(value);
-                }
-            }
+            get => GetValue(CaretIndexProperty);
+            set => SetValue(CaretIndexProperty, value);
         }
 
         public char PasswordChar
@@ -320,30 +291,14 @@ namespace Avalonia.Controls.Presenters
 
         public int SelectionStart
         {
-            get
-            {
-                return _selectionStart;
-            }
-
-            set
-            {
-                value = CoerceCaretIndex(value);
-                SetAndRaise(SelectionStartProperty, ref _selectionStart, value);
-            }
+            get => GetValue(SelectionStartProperty);
+            set => SetValue(SelectionStartProperty, value);
         }
 
         public int SelectionEnd
         {
-            get
-            {
-                return _selectionEnd;
-            }
-
-            set
-            {
-                value = CoerceCaretIndex(value);
-                SetAndRaise(SelectionEndProperty, ref _selectionEnd, value);
-            }
+            get => GetValue(SelectionEndProperty);
+            set => SetValue(SelectionEndProperty, value);
         }
 
         protected override bool BypassFlowDirectionPolicies => true;
@@ -535,12 +490,12 @@ namespace Avalonia.Controls.Presenters
         {
             TextLayout result;
 
-            var text = _text;
+            var text = Text;
 
             var typeface = new Typeface(FontFamily, FontStyle, FontWeight);
 
-            var selectionStart = CoerceCaretIndex(SelectionStart);
-            var selectionEnd = CoerceCaretIndex(SelectionEnd);
+            var selectionStart = SelectionStart;
+            var selectionEnd = SelectionEnd;
             var start = Math.Min(selectionStart, selectionEnd);
             var length = Math.Max(selectionStart, selectionEnd) - start;
 
@@ -561,9 +516,9 @@ namespace Avalonia.Controls.Presenters
                 };
 
             }
-            else if (!string.IsNullOrEmpty(_preeditText))
+            else if (!string.IsNullOrEmpty(PreeditText))
             {
-                var preeditHighlight = new ValueSpan<TextRunProperties>(_caretIndex, _preeditText.Length,
+                var preeditHighlight = new ValueSpan<TextRunProperties>(CaretIndex, PreeditText.Length,
                         new GenericTextRunProperties(typeface, FontSize,
                         foregroundBrush: foreground,
                         textDecorations: TextDecorations.Underline));
@@ -643,13 +598,6 @@ namespace Avalonia.Controls.Presenters
             return finalSize;
         }
 
-        private int CoerceCaretIndex(int value)
-        {
-            var text = Text;
-            var length = text?.Length ?? 0;
-            return Math.Max(0, Math.Min(length, value));
-        }
-
         private void CaretTimerTick(object? sender, EventArgs e)
         {
             _caretBlink = !_caretBlink;
@@ -865,7 +813,7 @@ namespace Avalonia.Controls.Presenters
 
             if (notify)
             {
-                SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex);
+                SetCurrentValue(CaretIndexProperty, caretIndex);
             }
         }
 
@@ -887,6 +835,11 @@ namespace Avalonia.Controls.Presenters
         {
             base.OnPropertyChanged(change);
 
+            if (change.Property == CaretIndexProperty)
+            {
+                MoveCaretToTextPosition(change.GetNewValue<int>());
+            }
+
             switch (change.Property.Name)
             {
                 case nameof(PreeditText):

+ 47 - 33
src/Avalonia.Controls/Primitives/Popup.cs

@@ -65,11 +65,17 @@ namespace Avalonia.Controls.Primitives
         public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
             AvaloniaProperty.Register<Popup, PopupGravity>(nameof(PlacementGravity));
 
+        /// <summary>
+        /// Defines the <see cref="Placement"/> property.
+        /// </summary>
+        public static readonly StyledProperty<PlacementMode> PlacementProperty =
+            AvaloniaProperty.Register<Popup, PlacementMode>(nameof(Placement), defaultValue: PlacementMode.Bottom);
+
         /// <summary>
         /// Defines the <see cref="PlacementMode"/> property.
         /// </summary>
-        public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
-            AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
+        [Obsolete("Use the Placement property instead.")]
+        public static readonly StyledProperty<PlacementMode> PlacementModeProperty = PlacementProperty;
 
         /// <summary>
         /// Defines the <see cref="PlacementRect"/> property.
@@ -146,8 +152,8 @@ namespace Avalonia.Controls.Primitives
 
         public bool WindowManagerAddShadowHint
         {
-            get { return GetValue(WindowManagerAddShadowHintProperty); }
-            set { SetValue(WindowManagerAddShadowHintProperty, value); }
+            get => GetValue(WindowManagerAddShadowHintProperty);
+            set => SetValue(WindowManagerAddShadowHintProperty, value);
         }
 
         /// <summary>
@@ -156,8 +162,8 @@ namespace Avalonia.Controls.Primitives
         [Content]
         public Control? Child
         {
-            get { return GetValue(ChildProperty); }
-            set { SetValue(ChildProperty, value); }
+            get => GetValue(ChildProperty);
+            set => SetValue(ChildProperty, value);
         }
 
         /// <summary>
@@ -205,13 +211,13 @@ namespace Avalonia.Controls.Primitives
         }
 
         /// <summary>
-        /// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="PlacementMode"/>
+        /// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="Placement"/>
         /// is <see cref="PlacementMode.AnchorAndGravity"/>.
         /// </summary>
         public PopupAnchor PlacementAnchor
         {
-            get { return GetValue(PlacementAnchorProperty); }
-            set { SetValue(PlacementAnchorProperty, value); }
+            get => GetValue(PlacementAnchorProperty);
+            set => SetValue(PlacementAnchorProperty, value);
         }
 
         /// <summary>
@@ -220,32 +226,40 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
         {
-            get { return GetValue(PlacementConstraintAdjustmentProperty); }
-            set { SetValue(PlacementConstraintAdjustmentProperty, value); }
+            get => GetValue(PlacementConstraintAdjustmentProperty);
+            set => SetValue(PlacementConstraintAdjustmentProperty, value);
         }
 
         /// <summary>
         /// Gets or sets a value which defines in what direction the popup should open
-        /// when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
+        /// when <see cref="Placement"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
         /// </summary>
         public PopupGravity PlacementGravity
         {
-            get { return GetValue(PlacementGravityProperty); }
-            set { SetValue(PlacementGravityProperty, value); }
+            get => GetValue(PlacementGravityProperty);
+            set => SetValue(PlacementGravityProperty, value);
+        }
+
+        /// <inheritdoc cref="Placement"/>
+        [Obsolete("Use the Placement property instead.")]
+        public PlacementMode PlacementMode
+        {
+            get => GetValue(PlacementProperty);
+            set => SetValue(PlacementProperty, value);
         }
 
         /// <summary>
-        /// Gets or sets the placement mode of the popup in relation to the <see cref="PlacementTarget"/>.
+        /// Gets or sets the desired placement of the popup in relation to the <see cref="PlacementTarget"/>.
         /// </summary>
-        public PlacementMode PlacementMode
+        public PlacementMode Placement
         {
-            get { return GetValue(PlacementModeProperty); }
-            set { SetValue(PlacementModeProperty, value); }
+            get => GetValue(PlacementProperty);
+            set => SetValue(PlacementProperty, value);
         }
 
         /// <summary>
         /// Gets or sets the the anchor rectangle within the parent that the popup will be placed
-        /// relative to when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
+        /// relative to when <see cref="Placement"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
         /// </summary>
         /// <remarks>
         /// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around
@@ -256,8 +270,8 @@ namespace Avalonia.Controls.Primitives
         /// </remarks>
         public Rect? PlacementRect
         {
-            get { return GetValue(PlacementRectProperty); }
-            set { SetValue(PlacementRectProperty, value); }
+            get => GetValue(PlacementRectProperty);
+            set => SetValue(PlacementRectProperty, value);
         }
 
         /// <summary>
@@ -266,8 +280,8 @@ namespace Avalonia.Controls.Primitives
         [ResolveByName]
         public Control? PlacementTarget
         {
-            get { return GetValue(PlacementTargetProperty); }
-            set { SetValue(PlacementTargetProperty, value); }
+            get => GetValue(PlacementTargetProperty);
+            set => SetValue(PlacementTargetProperty, value);
         }
 
         /// <summary>
@@ -301,8 +315,8 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public double HorizontalOffset
         {
-            get { return GetValue(HorizontalOffsetProperty); }
-            set { SetValue(HorizontalOffsetProperty, value); }
+            get => GetValue(HorizontalOffsetProperty);
+            set => SetValue(HorizontalOffsetProperty, value);
         }
 
         /// <summary>
@@ -310,8 +324,8 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public double VerticalOffset
         {
-            get { return GetValue(VerticalOffsetProperty); }
-            set { SetValue(VerticalOffsetProperty, value); }
+            get => GetValue(VerticalOffsetProperty);
+            set => SetValue(VerticalOffsetProperty, value);
         }
 
         /// <summary>
@@ -319,8 +333,8 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public bool Topmost
         {
-            get { return GetValue(TopmostProperty); }
-            set { SetValue(TopmostProperty, value); }
+            get => GetValue(TopmostProperty);
+            set => SetValue(TopmostProperty, value);
         }
 
         IPopupHost? IPopupHostProvider.PopupHost => Host;
@@ -404,7 +418,7 @@ namespace Avalonia.Controls.Primitives
                     (x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup);
 
                 // Recalculate popup position on parent moved/resized, but not if placement was on pointer
-                if (PlacementMode != PlacementMode.Pointer)
+                if (Placement != PlacementMode.Pointer)
                 {
                     SubscribeToEventHandler<IWindowImpl, Action<PixelPoint>>(window.PlatformImpl, WindowPositionChanged,
                         (x, handler) => x.PositionChanged += handler,
@@ -534,7 +548,7 @@ namespace Avalonia.Controls.Primitives
                     UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget);
                 }
                 else if (change.Property == PlacementTargetProperty ||
-                         change.Property == PlacementModeProperty ||
+                         change.Property == PlacementProperty ||
                          change.Property == HorizontalOffsetProperty ||
                          change.Property == VerticalOffsetProperty ||
                          change.Property == PlacementAnchorProperty ||
@@ -567,7 +581,7 @@ namespace Avalonia.Controls.Primitives
         {
             popupHost.ConfigurePosition(
                 placementTarget,
-                PlacementMode,
+                Placement,
                 new Point(HorizontalOffset, VerticalOffset),
                 PlacementAnchor,
                 PlacementGravity,
@@ -615,7 +629,7 @@ namespace Avalonia.Controls.Primitives
                     return;
                 _openState.PopupHost.ConfigurePosition(
                     placementTarget,
-                    PlacementMode,
+                    Placement,
                     new Point(HorizontalOffset, VerticalOffset),
                     PlacementAnchor,
                     PlacementGravity,

+ 2 - 1
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs

@@ -112,7 +112,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
                                    ?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry))
                                    ?? screens.FirstOrDefault();
 
-                if (targetScreen != null && targetScreen.WorkingArea.IsDefault)
+                if (targetScreen != null &&
+                    (targetScreen.WorkingArea.Width == 0 && targetScreen.WorkingArea.Height == 0))
                 {
                     return targetScreen.Bounds;
                 }

+ 29 - 26
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -145,6 +145,11 @@ namespace Avalonia.Controls.Primitives
         private BindingHelper? _bindingHelper;
         private bool _isSelectionChangeActive;
 
+        public SelectingItemsControl()
+        {
+            ((ItemCollection)ItemsView).SourceChanged += OnItemsViewSourceChanged;
+        }
+
         /// <summary>
         /// Initializes static members of the <see cref="SelectingItemsControl"/> class.
         /// </summary>
@@ -229,6 +234,7 @@ namespace Avalonia.Controls.Primitives
         /// <see cref="SelectedValue"/> property
         /// </summary>
         [AssignBinding]
+        [InheritDataTypeFromItems(nameof(ItemsSource))]
         [InheritDataTypeFromItems(nameof(Items))]
         public IBinding? SelectedValueBinding
         {
@@ -322,7 +328,7 @@ namespace Avalonia.Controls.Primitives
                 }
                 else if (_selection != value)
                 {
-                    if (value.Source != null && value.Source != Items)
+                    if (value.Source != null && value.Source != ItemsView.Source)
                     {
                         throw new ArgumentException(
                             "The supplied ISelectionModel already has an assigned Source but this " +
@@ -434,10 +440,9 @@ namespace Avalonia.Controls.Primitives
             return null;
         }
 
-        /// <inheritdoc />
-        protected override void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+        private protected override void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
         {
-            base.ItemsCollectionChanged(sender!, e);
+            base.OnItemsViewCollectionChanged(sender!, e);
 
             if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0)
             {
@@ -547,7 +552,7 @@ namespace Avalonia.Controls.Primitives
 
             if (_selection is object)
             {
-                _selection.Source = Items;
+                _selection.Source = ItemsView.Source;
             }
         }
 
@@ -635,16 +640,6 @@ namespace Avalonia.Controls.Primitives
             {
                 AutoScrollToSelectedItemIfNecessary();
             }
-            if (change.Property == ItemsProperty && _updateState is null && _selection is object)
-            {
-                var newValue = change.GetNewValue<IEnumerable?>();
-                _selection.Source = newValue;
-
-                if (newValue is null)
-                {
-                    _selection.Clear();
-                }
-            }
             else if (change.Property == SelectionModeProperty && _selection is object)
             {
                 var newValue = change.GetNewValue<SelectionMode>();
@@ -880,6 +875,12 @@ namespace Avalonia.Controls.Primitives
             return false;
         }
 
+        private void OnItemsViewSourceChanged(object? sender, EventArgs e)
+        {
+            if (_selection is not null && _updateState is null)
+                _selection.Source = ItemsView.Source;
+        }
+
         /// <summary>
         /// Called when <see cref="INotifyPropertyChanged.PropertyChanged"/> is raised on
         /// <see cref="Selection"/>.
@@ -968,7 +969,7 @@ namespace Avalonia.Controls.Primitives
         /// <param name="e">The event args.</param>
         private void OnSelectionModelLostSelection(object? sender, EventArgs e)
         {
-            if (AlwaysSelected && Items is object)
+            if (AlwaysSelected && ItemsView.Count > 0)
             {
                 SelectedIndex = 0;
             }
@@ -998,14 +999,14 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
-        private object FindItemWithValue(object? value)
+        private object? FindItemWithValue(object? value)
         {
             if (ItemCount == 0 || value is null)
             {
                 return AvaloniaProperty.UnsetValue;
             }
 
-            var items = Items;
+            var items = ItemsView;
             var binding = SelectedValueBinding;
 
             if (binding is null)
@@ -1169,7 +1170,7 @@ namespace Avalonia.Controls.Primitives
         {
             if (_updateState is null)
             {
-                model.Source = Items;
+                model.Source = ItemsView.Source;
             }
 
             model.PropertyChanged += OnSelectionModelPropertyChanged;
@@ -1231,16 +1232,18 @@ namespace Avalonia.Controls.Primitives
                     Selection = state.Selection.Value;
                 }
 
-                if (state.SelectedItems.HasValue)
+                if (_selection is InternalSelectionModel s)
                 {
-                    SelectedItems = state.SelectedItems.Value;
+                    s.Update(ItemsView.Source, state.SelectedItems);
                 }
-
-                Selection.Source = Items;
-
-                if (Items is null)
+                else
                 {
-                    Selection.Clear();
+                    if (state.SelectedItems.HasValue)
+                    {
+                        SelectedItems = state.SelectedItems.Value;
+                    }
+
+                    Selection.Source = ItemsView.Source;
                 }
 
                 if (state.SelectedValue.HasValue)

+ 46 - 61
src/Avalonia.Controls/SelectableTextBlock.cs

@@ -17,17 +17,11 @@ namespace Avalonia.Controls
     /// </summary>
     public class SelectableTextBlock : TextBlock, IInlineHost
     {
-        public static readonly DirectProperty<SelectableTextBlock, int> SelectionStartProperty =
-            AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>(
-                nameof(SelectionStart),
-                o => o.SelectionStart,
-                (o, v) => o.SelectionStart = v);
-
-        public static readonly DirectProperty<SelectableTextBlock, int> SelectionEndProperty =
-            AvaloniaProperty.RegisterDirect<SelectableTextBlock, int>(
-                nameof(SelectionEnd),
-                o => o.SelectionEnd,
-                (o, v) => o.SelectionEnd = v);
+        public static readonly StyledProperty<int> SelectionStartProperty =
+            TextBox.SelectionStartProperty.AddOwner<SelectableTextBlock>(new(coerce: TextBox.CoerceCaretIndex));
+
+        public static readonly StyledProperty<int> SelectionEndProperty =
+            TextBox.SelectionEndProperty.AddOwner<SelectableTextBlock>(new(coerce: TextBox.CoerceCaretIndex));
 
         public static readonly DirectProperty<SelectableTextBlock, string> SelectedTextProperty =
             AvaloniaProperty.RegisterDirect<SelectableTextBlock, string>(
@@ -35,21 +29,16 @@ namespace Avalonia.Controls
                 o => o.SelectedText);
 
         public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
-            AvaloniaProperty.Register<SelectableTextBlock, IBrush?>(nameof(SelectionBrush), Brushes.Blue);
-
+            TextBox.SelectionBrushProperty.AddOwner<SelectableTextBlock>(new(new Data.Optional<IBrush?>(Brushes.Blue)));
 
         public static readonly DirectProperty<SelectableTextBlock, bool> CanCopyProperty =
-            AvaloniaProperty.RegisterDirect<SelectableTextBlock, bool>(
-                nameof(CanCopy),
-                o => o.CanCopy);
+            TextBox.CanCopyProperty.AddOwner<SelectableTextBlock>(o => o.CanCopy);
 
         public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
             RoutedEvent.Register<SelectableTextBlock, RoutedEventArgs>(
                 nameof(CopyingToClipboard), RoutingStrategies.Bubble);
 
         private bool _canCopy;
-        private int _selectionStart;
-        private int _selectionEnd;
         private int _wordSelectionStart = -1;
 
         static SelectableTextBlock()
@@ -78,16 +67,8 @@ namespace Avalonia.Controls
         /// </summary>
         public int SelectionStart
         {
-            get => _selectionStart;
-            set
-            {
-                if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value))
-                {
-                    RaisePropertyChanged(SelectedTextProperty, "", "");
-
-                    UpdateCommandStates();
-                }
-            }
+            get => GetValue(SelectionStartProperty);
+            set => SetValue(SelectionStartProperty, value);
         }
 
         /// <summary>
@@ -95,16 +76,8 @@ namespace Avalonia.Controls
         /// </summary>
         public int SelectionEnd
         {
-            get => _selectionEnd;
-            set
-            {
-                if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value))
-                {
-                    RaisePropertyChanged(SelectedTextProperty, "", "");
-
-                    UpdateCommandStates();
-                }
-            }
+            get => GetValue(SelectionEndProperty);
+            set => SetValue(SelectionEndProperty, value);
         }
 
         /// <summary>
@@ -150,7 +123,7 @@ namespace Avalonia.Controls
                 await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
                     .SetTextAsync(text);
             }
-        }        
+        }
 
         /// <summary>
         /// Select all text in the TextBox
@@ -159,8 +132,8 @@ namespace Avalonia.Controls
         {
             var text = Text;
 
-            SelectionStart = 0;
-            SelectionEnd = text?.Length ?? 0;
+            SetCurrentValue(SelectionStartProperty, 0);
+            SetCurrentValue(SelectionEndProperty, text?.Length ?? 0);
         }
 
         /// <summary>
@@ -168,7 +141,7 @@ namespace Avalonia.Controls
         /// </summary>
         public void ClearSelection()
         {
-            SelectionEnd = SelectionStart;
+            SetCurrentValue(SelectionEndProperty, SelectionStart);
         }
 
         protected override void OnGotFocus(GotFocusEventArgs e)
@@ -204,7 +177,7 @@ namespace Avalonia.Controls
 
                 var rects = TextLayout.HitTestTextRange(start, length);
 
-                using (context.PushPostTransform(Matrix.CreateTranslation(origin)))
+                using (context.PushTransform(Matrix.CreateTranslation(origin)))
                 {
                     foreach (var rect in rects)
                     {
@@ -240,6 +213,17 @@ namespace Avalonia.Controls
             e.Handled = handled;
         }
 
+        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+        {
+            base.OnPropertyChanged(change);
+
+            if (change.Property == SelectionStartProperty || change.Property == SelectionEndProperty)
+            {
+                RaisePropertyChanged(SelectedTextProperty, "", "");
+                UpdateCommandStates();
+            }
+        }
+
         protected override void OnPointerPressed(PointerPressedEventArgs e)
         {
             base.OnPointerPressed(e);
@@ -271,25 +255,26 @@ namespace Avalonia.Controls
 
                                 if (index > _wordSelectionStart)
                                 {
-                                    SelectionEnd = StringUtils.NextWord(text, index);
+                                    SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
                                 }
 
                                 if (index < _wordSelectionStart || previousWord == _wordSelectionStart)
                                 {
-                                    SelectionStart = previousWord;
+                                    SetCurrentValue(SelectionStartProperty, previousWord);
                                 }
                             }
                             else
                             {
-                                SelectionStart = Math.Min(oldIndex, index);
-                                SelectionEnd = Math.Max(oldIndex, index);
+                                SetCurrentValue(SelectionStartProperty, Math.Min(oldIndex, index));
+                                SetCurrentValue(SelectionEndProperty, Math.Max(oldIndex, index));
                             }
                         }
                         else
                         {
                             if (_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd)
                             {
-                                SelectionStart = SelectionEnd = index;
+                                SetCurrentValue(SelectionStartProperty, index);
+                                SetCurrentValue(SelectionEndProperty, index);
 
                                 _wordSelectionStart = -1;
                             }
@@ -299,16 +284,16 @@ namespace Avalonia.Controls
                     case 2:
                         if (!StringUtils.IsStartOfWord(text, index))
                         {
-                            SelectionStart = StringUtils.PreviousWord(text, index);
+                            SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, index));
                         }
 
                         _wordSelectionStart = SelectionStart;
 
                         if (!StringUtils.IsEndOfWord(text, index))
                         {
-                            SelectionEnd = StringUtils.NextWord(text, index);
+                            SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
                         }
-                        
+
                         break;
                     case 3:
                         _wordSelectionStart = -1;
@@ -347,22 +332,22 @@ namespace Avalonia.Controls
 
                     if (distance <= 0)
                     {
-                        SelectionStart = StringUtils.PreviousWord(text, textPosition);
+                        SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, textPosition));
                     }
 
                     if (distance >= 0)
                     {
                         if (SelectionStart != _wordSelectionStart)
                         {
-                            SelectionStart = _wordSelectionStart;
+                            SetCurrentValue(SelectionStartProperty, _wordSelectionStart);
                         }
 
-                        SelectionEnd = StringUtils.NextWord(text, textPosition);
+                        SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, textPosition));
                     }
                 }
                 else
                 {
-                    SelectionEnd = textPosition;
+                    SetCurrentValue(SelectionEndProperty, textPosition);
                 }
 
             }
@@ -395,7 +380,8 @@ namespace Avalonia.Controls
                                           caretIndex >= firstSelection && caretIndex <= lastSelection;
                 if (!didClickInSelection)
                 {
-                    SelectionStart = SelectionEnd = caretIndex;
+                    SetCurrentValue(SelectionStartProperty, caretIndex);
+                    SetCurrentValue(SelectionEndProperty, caretIndex);
                 }
             }
 
@@ -411,9 +397,8 @@ namespace Avalonia.Controls
 
         private string GetSelection()
         {
-            var text = GetText();
-
-            if (string.IsNullOrEmpty(text))
+            var textLength = Text?.Length ?? 0;
+            if (textLength == 0)
             {
                 return "";
             }
@@ -423,14 +408,14 @@ namespace Avalonia.Controls
             var start = Math.Min(selectionStart, selectionEnd);
             var end = Math.Max(selectionStart, selectionEnd);
 
-            if (start == end || text.Length < end)
+            if (start == end || textLength < end)
             {
                 return "";
             }
 
             var length = Math.Max(0, end - start);
 
-            var selectedText = text.Substring(start, length);
+            var selectedText = Text!.Substring(start, length);
 
             return selectedText;
         }

+ 26 - 1
src/Avalonia.Controls/Selection/InternalSelectionModel.cs

@@ -5,6 +5,7 @@ using System.Collections.Specialized;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Avalonia.Collections;
+using Avalonia.Data;
 
 namespace Avalonia.Controls.Selection
 {
@@ -13,6 +14,7 @@ namespace Avalonia.Controls.Selection
         private IList? _writableSelectedItems;
         private int _ignoreModelChanges;
         private bool _ignoreSelectedItemsChanges;
+        private bool _skipSyncFromSelectedItems;
         private bool _isResetting;
 
         public InternalSelectionModel()
@@ -60,6 +62,29 @@ namespace Avalonia.Controls.Selection
             }
         }
 
+        internal void Update(IEnumerable? source, Optional<IList?> selectedItems)
+        {
+            var previousSource = Source;
+            var previousWritableSelectedItems = _writableSelectedItems;
+
+            try
+            {
+                _skipSyncFromSelectedItems = true;
+                SetSource(source);
+                if (selectedItems.HasValue)
+                    WritableSelectedItems = selectedItems.Value;
+            }
+            finally 
+            { 
+                _skipSyncFromSelectedItems = false;
+            }
+
+            // We skipped the sync from WritableSelectedItems before; do it now that both
+            // the source and WritableSelectedItems are updated.
+            if (previousSource != Source || previousWritableSelectedItems != _writableSelectedItems)
+                SyncFromSelectedItems();
+        }
+
         private protected override void SetSource(IEnumerable? value)
         {
             if (Source == value)
@@ -121,7 +146,7 @@ namespace Avalonia.Controls.Selection
 
         private void SyncFromSelectedItems()
         {
-            if (Source is null || _writableSelectedItems is null)
+            if (_skipSyncFromSelectedItems || Source is null || _writableSelectedItems is null)
             {
                 return;
             }

+ 2 - 2
src/Avalonia.Controls/Selection/SelectionModel.cs

@@ -30,9 +30,9 @@ namespace Avalonia.Controls.Selection
             Source = source;
         }
 
-        public new IEnumerable<T>? Source
+        public new IEnumerable? Source
         {
-            get => base.Source as IEnumerable<T>;
+            get => base.Source;
             set => SetSource(value);
         }
 

+ 1 - 1
src/Avalonia.Controls/SplitButton/SplitButton.cs

@@ -172,7 +172,7 @@ namespace Avalonia.Controls
                 flyout.Opened += Flyout_Opened;
                 flyout.Closed += Flyout_Closed;
 
-                _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(Popup.PlacementModeProperty).Subscribe(Flyout_PlacementPropertyChanged);
+                _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(Popup.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged);
             }
         }
 

+ 37 - 29
src/Avalonia.Controls/TextBlock.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using Avalonia.Automation.Peers;
 using Avalonia.Controls.Documents;
 using Avalonia.Layout;
@@ -13,6 +14,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// A control that displays a block of text.
     /// </summary>
+    [DebuggerDisplay("Text = {" + nameof(DebugText) + "}")]
     public class TextBlock : Control, IInlineHost
     {
         /// <summary>
@@ -103,11 +105,8 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="Text"/> property.
         /// </summary>
-        public static readonly DirectProperty<TextBlock, string?> TextProperty =
-            AvaloniaProperty.RegisterDirect<TextBlock, string?>(
-                nameof(Text),
-                o => o.GetText(),
-                (o, v) => o.SetText(v));
+        public static readonly StyledProperty<string?> TextProperty =
+            AvaloniaProperty.Register<TextBlock, string?>(nameof(Text));
 
         /// <summary>
         /// Defines the <see cref="TextAlignment"/> property.
@@ -142,14 +141,14 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="Inlines"/> property.
         /// </summary>
-        public static readonly StyledProperty<InlineCollection?> InlinesProperty =
-            AvaloniaProperty.Register<TextBlock, InlineCollection?>(
-                nameof(Inlines));
+        public static readonly DirectProperty<TextBlock, InlineCollection?> InlinesProperty =
+            AvaloniaProperty.RegisterDirect<TextBlock, InlineCollection?>(
+                nameof(Inlines), t => t.Inlines, (t, v) => t.Inlines = v);
 
-        internal string? _text;
         protected TextLayout? _textLayout;
         protected Size _constraint;
         private IReadOnlyList<TextRun>? _textRuns;
+        private InlineCollection? _inlines;
 
         /// <summary>
         /// Initializes static members of the <see cref="TextBlock"/> class.
@@ -173,7 +172,7 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets the <see cref="TextLayout"/> used to render the text.
         /// </summary>
-        public TextLayout TextLayout => _textLayout ??= CreateTextLayout(_text);
+        public TextLayout TextLayout => _textLayout ??= CreateTextLayout(Text);
 
         /// <summary>
         /// Gets or sets the padding to place around the <see cref="Text"/>.
@@ -198,10 +197,12 @@ namespace Avalonia.Controls
         /// </summary>
         public string? Text
         {
-            get => GetText();
-            set => SetText(value);
+            get => GetValue(TextProperty);
+            set => SetValue(TextProperty, value);
         }
 
+        private string? DebugText => Text ?? Inlines?.Text;
+
         /// <summary>
         /// Gets or sets the font family used to draw the control's text.
         /// </summary>
@@ -325,8 +326,8 @@ namespace Avalonia.Controls
         [Content]
         public InlineCollection? Inlines
         {
-            get => GetValue(InlinesProperty);
-            set => SetValue(InlinesProperty, value);
+            get => _inlines;
+            set => SetAndRaise(InlinesProperty, ref _inlines, value);
         }
 
         protected override bool BypassFlowDirectionPolicies => true;
@@ -590,19 +591,18 @@ namespace Avalonia.Controls
             TextLayout.Draw(context, origin);
         }
 
-        protected virtual string? GetText()
-        {
-            return _text ?? Inlines?.Text;
-        }
-
-        protected virtual void SetText(string? text)
+        private bool _clearTextInternal;
+        internal void ClearTextInternal()
         {
-            if (HasComplexContent)
+            _clearTextInternal = true;
+            try
+            {
+                SetCurrentValue(TextProperty, null);
+            }
+            finally
             {
-                Inlines?.Clear();
+                _clearTextInternal = false;
             }
-           
-            SetAndRaise(TextProperty, ref _text, text);           
         }
 
         /// <summary>
@@ -780,6 +780,14 @@ namespace Avalonia.Controls
         {
             base.OnPropertyChanged(change);
 
+            if (change.Property == TextProperty)
+            {
+                if (HasComplexContent && !_clearTextInternal)
+                {
+                    Inlines?.Clear();
+                }
+            }
+
             switch (change.Property.Name)
             {
                 case nameof(FontSize):
@@ -794,10 +802,10 @@ namespace Avalonia.Controls
 
                 case nameof(FlowDirection):
 
-                case nameof (Padding):
-                case nameof (LineHeight):
-                case nameof (LetterSpacing):
-                case nameof (MaxLines):
+                case nameof(Padding):
+                case nameof(LineHeight):
+                case nameof(LetterSpacing):
+                case nameof(MaxLines):
 
                 case nameof(Text):
                 case nameof(TextDecorations):
@@ -899,7 +907,7 @@ namespace Avalonia.Controls
                         continue;
                     }
 
-                    if (textRun is TextCharacters)                 
+                    if (textRun is TextCharacters)
                     {
                         var skip = Math.Max(0, textSourceIndex - currentPosition);
 

+ 213 - 220
src/Avalonia.Controls/TextBox.cs

@@ -61,11 +61,9 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="CaretIndex"/> property
         /// </summary>
-        public static readonly DirectProperty<TextBox, int> CaretIndexProperty =
-            AvaloniaProperty.RegisterDirect<TextBox, int>(
-                nameof(CaretIndex),
-                o => o.CaretIndex,
-                (o, v) => o.CaretIndex = v);
+        public static readonly StyledProperty<int> CaretIndexProperty =
+            AvaloniaProperty.Register<TextBox, int>(nameof(CaretIndex),
+                coerce: CoerceCaretIndex);
 
         /// <summary>
         /// Defines the <see cref="IsReadOnly"/> property
@@ -100,42 +98,37 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="SelectionStart"/> property
         /// </summary>
-        public static readonly DirectProperty<TextBox, int> SelectionStartProperty =
-            AvaloniaProperty.RegisterDirect<TextBox, int>(
-                nameof(SelectionStart),
-                o => o.SelectionStart,
-                (o, v) => o.SelectionStart = v);
+        public static readonly StyledProperty<int> SelectionStartProperty =
+            AvaloniaProperty.Register<TextBox, int>(nameof(SelectionStart), 
+                coerce: CoerceCaretIndex);
 
         /// <summary>
         /// Defines the <see cref="SelectionEnd"/> property
         /// </summary>
-        public static readonly DirectProperty<TextBox, int> SelectionEndProperty =
-            AvaloniaProperty.RegisterDirect<TextBox, int>(
-                nameof(SelectionEnd),
-                o => o.SelectionEnd,
-                (o, v) => o.SelectionEnd = v);
+        public static readonly StyledProperty<int> SelectionEndProperty =
+            AvaloniaProperty.Register<TextBox, int>(nameof(SelectionEnd),
+                coerce: CoerceCaretIndex);
 
         /// <summary>
         /// Defines the <see cref="MaxLength"/> property
         /// </summary>
         public static readonly StyledProperty<int> MaxLengthProperty =
-            AvaloniaProperty.Register<TextBox, int>(nameof(MaxLength), defaultValue: 0);
+            AvaloniaProperty.Register<TextBox, int>(nameof(MaxLength));
 
         /// <summary>
         /// Defines the <see cref="MaxLines"/> property
         /// </summary>
         public static readonly StyledProperty<int> MaxLinesProperty =
-            AvaloniaProperty.Register<TextBox, int>(nameof(MaxLines), defaultValue: 0);
+            AvaloniaProperty.Register<TextBox, int>(nameof(MaxLines));
 
         /// <summary>
         /// Defines the <see cref="Text"/> property
         /// </summary>
-        public static readonly DirectProperty<TextBox, string?> TextProperty =
-            TextBlock.TextProperty.AddOwnerWithDataValidation<TextBox>(
-                o => o.Text,
-                (o, v) => o.Text = v,
+        public static readonly StyledProperty<string?> TextProperty =
+            TextBlock.TextProperty.AddOwner<TextBox>(new(
+                coerce: CoerceText,
                 defaultBindingMode: BindingMode.TwoWay,
-                enableDataValidation: true);
+                enableDataValidation: true));
 
         /// <summary>
         /// Defines the <see cref="TextAlignment"/> property
@@ -185,9 +178,8 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="NewLine"/> property
         /// </summary>
-        public static readonly DirectProperty<TextBox, string> NewLineProperty =
-            AvaloniaProperty.RegisterDirect<TextBox, string>(nameof(NewLine),
-                textbox => textbox.NewLine, (textbox, newline) => textbox.NewLine = newline);
+        public static readonly StyledProperty<string> NewLineProperty =
+            AvaloniaProperty.Register<TextBox, string>(nameof(NewLine), Environment.NewLine);
 
         /// <summary>
         /// Defines the <see cref="InnerLeftContent"/> property
@@ -242,12 +234,8 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="UndoLimit"/> property
         /// </summary>
-        public static readonly DirectProperty<TextBox, int> UndoLimitProperty =
-            AvaloniaProperty.RegisterDirect<TextBox, int>(
-                nameof(UndoLimit),
-                o => o.UndoLimit,
-                (o, v) => o.UndoLimit = v,
-                unsetValue: -1);
+        public static readonly StyledProperty<int> UndoLimitProperty =
+            AvaloniaProperty.Register<TextBox, int>(nameof(UndoLimit), UndoRedoHelper<UndoRedoState>.DefaultUndoLimit);
 
         /// <summary>
         /// Defines the <see cref="CanUndo"/> property
@@ -318,18 +306,13 @@ namespace Avalonia.Controls
             public override int GetHashCode() => Text?.GetHashCode() ?? 0;
         }
 
-        private string? _text;
-        private int _caretIndex;
-        private int _selectionStart;
-        private int _selectionEnd;
         private TextPresenter? _presenter;
-        private TextBoxTextInputMethodClient _imClient = new TextBoxTextInputMethodClient();
-        private UndoRedoHelper<UndoRedoState> _undoRedoHelper;
+        private readonly TextBoxTextInputMethodClient _imClient = new();
+        private readonly UndoRedoHelper<UndoRedoState> _undoRedoHelper;
         private bool _isUndoingRedoing;
         private bool _canCut;
         private bool _canCopy;
         private bool _canPaste;
-        private string _newLine = Environment.NewLine;
         private static readonly string[] invalidCharacters = new String[1] { "\u007f" };
         private bool _canUndo;
         private bool _canRedo;
@@ -399,18 +382,19 @@ namespace Avalonia.Controls
         /// </summary>
         public int CaretIndex
         {
-            get => _caretIndex;
-            set
-            {
-                value = CoerceCaretIndex(value);
-                SetAndRaise(CaretIndexProperty, ref _caretIndex, value);
+            get => GetValue(CaretIndexProperty);
+            set => SetValue(CaretIndexProperty, value);
+        }
 
-                UndoRedoState state;
-                if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text)
-                    _undoRedoHelper.UpdateLastState();
+        private void OnCaretIndexChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            UndoRedoState state;
+            if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text)
+                _undoRedoHelper.UpdateLastState();
 
-                SelectionStart = SelectionEnd = value;
-            }
+            var newValue = e.GetNewValue<int>();
+            SetCurrentValue(SelectionStartProperty, newValue);
+            SetCurrentValue(SelectionEndProperty, newValue);
         }
 
         /// <summary>
@@ -463,21 +447,18 @@ namespace Avalonia.Controls
         /// </summary>
         public int SelectionStart
         {
-            get => _selectionStart;
-            set
-            {
-                value = CoerceCaretIndex(value);
-                var changed = SetAndRaise(SelectionStartProperty, ref _selectionStart, value);
+            get => GetValue(SelectionStartProperty);
+            set => SetValue(SelectionStartProperty, value);
+        }
 
-                if (changed)
-                {
-                    UpdateCommandStates();
-                }
+        private void OnSelectionStartChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            UpdateCommandStates();
 
-                if (SelectionEnd == value && CaretIndex != value)
-                {
-                    CaretIndex = value;
-                }
+            var value = e.GetNewValue<int>();
+            if (SelectionEnd == value && CaretIndex != value)
+            {
+                SetCurrentValue(CaretIndexProperty, value);
             }
         }
 
@@ -490,21 +471,18 @@ namespace Avalonia.Controls
         /// </remarks>
         public int SelectionEnd
         {
-            get => _selectionEnd;
-            set
-            {
-                value = CoerceCaretIndex(value);
-                var changed = SetAndRaise(SelectionEndProperty, ref _selectionEnd, value);
-
-                if (changed)
-                {
-                    UpdateCommandStates();
-                }
+            get => GetValue(SelectionEndProperty);
+            set => SetValue(SelectionEndProperty, value);
+        }
+        
+        private void OnSelectionEndChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            UpdateCommandStates();
 
-                if (SelectionStart == value && CaretIndex != value)
-                {
-                    CaretIndex = value;
-                }
+            var value = e.GetNewValue<int>();
+            if (SelectionStart == value && CaretIndex != value)
+            {
+                SetCurrentValue(CaretIndexProperty, value);
             }
         }
         
@@ -550,36 +528,27 @@ namespace Avalonia.Controls
         [Content]
         public string? Text
         {
-            get => _text;
-            set
-            {
-                var caretIndex = CaretIndex;
-                var selectionStart = SelectionStart;
-                var selectionEnd = SelectionEnd;
-
-                CaretIndex = CoerceCaretIndex(caretIndex, value);
-                SelectionStart = CoerceCaretIndex(selectionStart, value);
-                SelectionEnd = CoerceCaretIndex(selectionEnd, value);
-
-                // Before #9490, snapshot here was done AFTER text change - this doesn't make sense
-                // since intial state would never be no text and you'd always have to make a text 
-                // change before undo would be available
-                // The undo/redo stacks were also cleared at this point, which also doesn't make sense
-                // as it is still valid to want to undo a programmatic text set
-                // So we snapshot text now BEFORE the change so we can always revert
-                // Also don't need to check IsUndoEnabled here, that's done in SnapshotUndoRedo
-                if (!_isUndoingRedoing)
-                {
-                    SnapshotUndoRedo();
-                }
-
-                var textChanged = SetAndRaise(TextProperty, ref _text, value);
+            get => GetValue(TextProperty);
+            set => SetValue(TextProperty, value);
+        }
 
-                if (textChanged)
-                {
-                    RaiseTextChangeEvents();
-                }
+        private static string? CoerceText(AvaloniaObject sender, string? value)
+        {
+            var textBox = (TextBox)sender;
+            
+            // Before #9490, snapshot here was done AFTER text change - this doesn't make sense
+            // since intial state would never be no text and you'd always have to make a text 
+            // change before undo would be available
+            // The undo/redo stacks were also cleared at this point, which also doesn't make sense
+            // as it is still valid to want to undo a programmatic text set
+            // So we snapshot text now BEFORE the change so we can always revert
+            // Also don't need to check IsUndoEnabled here, that's done in SnapshotUndoRedo
+            if (!textBox._isUndoingRedoing)
+            {
+                textBox.SnapshotUndoRedo();
             }
+
+            return value;
         }
 
         /// <summary>
@@ -691,8 +660,8 @@ namespace Avalonia.Controls
         /// </summary>
         public string NewLine
         {
-            get => _newLine;
-            set => SetAndRaise(NewLineProperty, ref _newLine, value);
+            get => GetValue(NewLineProperty);
+            set => SetValue(NewLineProperty, value);
         }
 
         /// <summary>
@@ -700,7 +669,8 @@ namespace Avalonia.Controls
         /// </summary>
         public void ClearSelection()
         {
-            CaretIndex = SelectionStart;
+            SetCurrentValue(CaretIndexProperty, SelectionStart);
+            SetCurrentValue(SelectionEndProperty, SelectionStart);
         }
 
         /// <summary>
@@ -744,25 +714,20 @@ namespace Avalonia.Controls
         /// </summary>
         public int UndoLimit
         {
-            get => _undoRedoHelper.Limit;
-            set
-            {
-                if (_undoRedoHelper.Limit != value)
-                {
-                    // can't use SetAndRaise due to using _undoRedoHelper.Limit
-                    // (can't send a ref of a property to SetAndRaise),
-                    // so use RaisePropertyChanged instead.
-                    var oldValue = _undoRedoHelper.Limit;
-                    _undoRedoHelper.Limit = value;
-                    RaisePropertyChanged(UndoLimitProperty, oldValue, value);
-                }
-                // from docs at
-                // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled:
-                // "Setting UndoLimit clears the undo queue."
-                _undoRedoHelper.Clear();
-                _selectedTextChangesMadeSinceLastUndoSnapshot = 0;
-                _hasDoneSnapshotOnce = false;
-            }
+            get => GetValue(UndoLimitProperty);
+            set => SetValue(UndoLimitProperty, value);
+        }
+
+        private void OnUndoLimitChanged(int newValue)
+        {
+            _undoRedoHelper.Limit = newValue;
+
+            // from docs at
+            // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled:
+            // "Setting UndoLimit clears the undo queue."
+            _undoRedoHelper.Clear();
+            _selectedTextChangesMadeSinceLastUndoSnapshot = 0;
+            _hasDoneSnapshotOnce = false;
         }
 
         /// <summary>
@@ -866,9 +831,31 @@ namespace Avalonia.Controls
 
             if (change.Property == TextProperty)
             {
+                CoerceValue(CaretIndexProperty);
+                CoerceValue(SelectionStartProperty);
+                CoerceValue(SelectionEndProperty);
+
+                RaiseTextChangeEvents();
+
                 UpdatePseudoclasses();
                 UpdateCommandStates();
             }
+            else if (change.Property == CaretIndexProperty)
+            {
+                OnCaretIndexChanged(change);
+            }
+            else if (change.Property == SelectionStartProperty)
+            {
+                OnSelectionStartChanged(change);
+            }
+            else if (change.Property == SelectionEndProperty)
+            {
+                OnSelectionEndChanged(change);
+            }
+            else if (change.Property == UndoLimitProperty)
+            {
+                OnUndoLimitChanged(change.GetNewValue<int>());
+            }
             else if (change.Property == IsUndoEnabledProperty && change.GetNewValue<bool>() == false)
             {
                 // from docs at
@@ -920,7 +907,7 @@ namespace Avalonia.Controls
                 (ContextMenu == null || !ContextMenu.IsOpen))
             {
                 ClearSelection();
-                RevealPassword = false;
+                SetCurrentValue(RevealPasswordProperty, false);
             }
 
             UpdateCommandStates();
@@ -986,35 +973,44 @@ namespace Avalonia.Controls
                 }
             }
 
-            var text = Text ?? string.Empty;
-            var newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd);
+            var currentText = Text ?? string.Empty;
+            var selectionLength = Math.Abs(SelectionStart - SelectionEnd);
+            var newLength = input.Length + currentText.Length - selectionLength;
 
             if (MaxLength > 0 && newLength > MaxLength)
             {
                 input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength)));
+                newLength = MaxLength;
             }
 
             if (!string.IsNullOrEmpty(input))
             {
-                var oldText = _text;
-
-                DeleteSelection(false);
+                var textBuilder = StringBuilderCache.Acquire(Math.Max(currentText.Length, newLength));
+                textBuilder.Append(currentText);
+                
                 var caretIndex = CaretIndex;
-                text = Text ?? string.Empty;
-                SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
-                ClearSelection();
 
-                if (IsUndoEnabled)
+                if (selectionLength != 0)
                 {
-                    _undoRedoHelper.DiscardRedo();
+                    var (start, _) = GetSelectionRange();
+
+                    textBuilder.Remove(start, selectionLength);
+
+                    caretIndex = start;
                 }
 
-                if (_text != oldText)
+                textBuilder.Insert(caretIndex, input);
+
+                SetCurrentValue(TextProperty, StringBuilderCache.GetStringAndRelease(textBuilder));
+
+                ClearSelection();
+
+                if (IsUndoEnabled)
                 {
-                    RaisePropertyChanged(TextProperty, oldText, _text);
+                    _undoRedoHelper.DiscardRedo();
                 }
 
-                CaretIndex = caretIndex + input.Length;
+                SetCurrentValue(CaretIndexProperty, caretIndex + input.Length);
             }
         }
 
@@ -1168,7 +1164,7 @@ namespace Avalonia.Controls
                 movement = true;
                 selection = false;
                 handled = true;
-                CaretIndex = _presenter.CaretIndex;
+                SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
             }
             else if (Match(keymap.MoveCursorToTheEndOfDocument))
             {
@@ -1176,7 +1172,7 @@ namespace Avalonia.Controls
                 movement = true;
                 selection = false;
                 handled = true;
-                CaretIndex = _presenter.CaretIndex;
+                SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
             }
             else if (Match(keymap.MoveCursorToTheStartOfLine))
             {
@@ -1184,7 +1180,7 @@ namespace Avalonia.Controls
                 movement = true;
                 selection = false;
                 handled = true;
-                CaretIndex = _presenter.CaretIndex;
+                SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
             }
             else if (Match(keymap.MoveCursorToTheEndOfLine))
             {
@@ -1192,31 +1188,31 @@ namespace Avalonia.Controls
                 movement = true;
                 selection = false;
                 handled = true;
-                CaretIndex = _presenter.CaretIndex;
+                SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
             }
             else if (Match(keymap.MoveCursorToTheStartOfDocumentWithSelection))
             {
-                SelectionStart = caretIndex;
+                SetCurrentValue(SelectionStartProperty, caretIndex);
                 MoveHome(true);
-                SelectionEnd = _presenter.CaretIndex;
+                SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
                 movement = true;
                 selection = true;
                 handled = true;
             }
             else if (Match(keymap.MoveCursorToTheEndOfDocumentWithSelection))
             {
-                SelectionStart = caretIndex;
+                SetCurrentValue(SelectionStartProperty, caretIndex);
                 MoveEnd(true);
-                SelectionEnd = _presenter.CaretIndex;
+                SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
                 movement = true;
                 selection = true;
                 handled = true;
             }
             else if (Match(keymap.MoveCursorToTheStartOfLineWithSelection))
             {
-                SelectionStart = caretIndex;
+                SetCurrentValue(SelectionStartProperty, caretIndex);
                 MoveHome(false);
-                SelectionEnd = _presenter.CaretIndex;
+                SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
                 movement = true;
                 selection = true;
                 handled = true;
@@ -1224,9 +1220,9 @@ namespace Avalonia.Controls
             }
             else if (Match(keymap.MoveCursorToTheEndOfLineWithSelection))
             {
-                SelectionStart = caretIndex;
+                SetCurrentValue(SelectionStartProperty, caretIndex);
                 MoveEnd(false);
-                SelectionEnd = _presenter.CaretIndex;
+                SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
                 movement = true;
                 selection = true;
                 handled = true;
@@ -1261,11 +1257,11 @@ namespace Avalonia.Controls
 
                             if (selection)
                             {
-                                SelectionEnd = _presenter.CaretIndex;
+                                SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
                             }
                             else
                             {
-                                CaretIndex = _presenter.CaretIndex;
+                                SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
                             }
 
                             break;
@@ -1283,11 +1279,11 @@ namespace Avalonia.Controls
 
                             if (selection)
                             {
-                                SelectionEnd = _presenter.CaretIndex;
+                                SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
                             }
                             else
                             {
-                                CaretIndex = _presenter.CaretIndex;
+                                SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
                             }
 
                             break;
@@ -1314,11 +1310,13 @@ namespace Avalonia.Controls
 
                                     var length = end - start;
 
-                                    var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length));
+                                    var sb = StringBuilderCache.Acquire(text.Length);
+                                    sb.Append(text);
+                                    sb.Remove(start, end - start);
 
-                                    SetTextInternal(editedText);
+                                    SetCurrentValue(TextProperty, StringBuilderCache.GetStringAndRelease(sb));
 
-                                    CaretIndex = start;
+                                    SetCurrentValue(CaretIndexProperty, start);
                                 }
                             }
 
@@ -1346,9 +1344,11 @@ namespace Avalonia.Controls
                                 var start = Math.Min(nextPosition, caretIndex);
                                 var end = Math.Max(nextPosition, caretIndex);
 
-                                var editedText = text.Substring(0, start) + text.Substring(Math.Min(end, text.Length));
+                                var sb = StringBuilderCache.Acquire(text.Length);
+                                sb.Append(text);
+                                sb.Remove(start, end - start);
 
-                                SetTextInternal(editedText);
+                                SetCurrentValue(TextProperty, StringBuilderCache.GetStringAndRelease(sb));
                             }
                         }
 
@@ -1425,7 +1425,7 @@ namespace Avalonia.Controls
 
                 var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
 
-                SetAndRaise(CaretIndexProperty, ref _caretIndex, index);
+                SetCurrentValue(CaretIndexProperty, index);
 
                 switch (e.ClickCount)
                 {
@@ -1438,25 +1438,26 @@ namespace Avalonia.Controls
 
                                 if (index > _wordSelectionStart)
                                 {
-                                    SelectionEnd = StringUtils.NextWord(text, index);
+                                    SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
                                 }
 
                                 if (index < _wordSelectionStart || previousWord == _wordSelectionStart)
                                 {
-                                    SelectionStart = previousWord;
+                                    SetCurrentValue(SelectionStartProperty, previousWord);
                                 }
                             }
                             else
                             {
-                                SelectionStart = Math.Min(oldIndex, index);
-                                SelectionEnd = Math.Max(oldIndex, index);
+                                SetCurrentValue(SelectionStartProperty, Math.Min(oldIndex, index));
+                                SetCurrentValue(SelectionEndProperty, Math.Max(oldIndex, index));
                             }
                         }
                         else
                         {
                             if(_wordSelectionStart == -1 || index < SelectionStart || index > SelectionEnd)
                             {
-                                SelectionStart = SelectionEnd = index;
+                                SetCurrentValue(SelectionStartProperty, index);
+                                SetCurrentValue(SelectionEndProperty, index);
                                 _wordSelectionStart = -1;
                             }                           
                         }
@@ -1466,14 +1467,14 @@ namespace Avalonia.Controls
 
                         if (!StringUtils.IsStartOfWord(text, index))
                         {
-                            SelectionStart = StringUtils.PreviousWord(text, index);
+                            SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, index));
                         }
 
                         _wordSelectionStart = SelectionStart;
 
                         if (!StringUtils.IsEndOfWord(text, index))
                         {
-                            SelectionEnd = StringUtils.NextWord(text, index);
+                            SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, index));
                         }
 
                         break;
@@ -1517,22 +1518,22 @@ namespace Avalonia.Controls
 
                     if (distance <= 0)
                     {
-                        SelectionStart = StringUtils.PreviousWord(text, caretIndex);
+                        SetCurrentValue(SelectionStartProperty, StringUtils.PreviousWord(text, caretIndex));
                     }
 
                     if (distance >= 0)
                     {
                         if(SelectionStart != _wordSelectionStart)
                         {
-                            SelectionStart = _wordSelectionStart;
+                            SetCurrentValue(SelectionStartProperty, _wordSelectionStart);
                         }
 
-                        SelectionEnd = StringUtils.NextWord(text, caretIndex);
+                        SetCurrentValue(SelectionEndProperty, StringUtils.NextWord(text, caretIndex));
                     }
                 }
                 else
                 {
-                    SelectionEnd = caretIndex;
+                    SetCurrentValue(SelectionEndProperty, caretIndex);
                 }
             }
         }
@@ -1565,7 +1566,9 @@ namespace Avalonia.Controls
                                           caretIndex >= firstSelection && caretIndex <= lastSelection;
                 if (!didClickInSelection)
                 {
-                    CaretIndex = SelectionEnd = SelectionStart = caretIndex;
+                    SetCurrentValue(CaretIndexProperty, caretIndex);
+                    SetCurrentValue(SelectionEndProperty, caretIndex);
+                    SetCurrentValue(SelectionStartProperty, caretIndex);
                 }
             }
 
@@ -1588,10 +1591,10 @@ namespace Avalonia.Controls
             }
         }
 
-        private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text);
-
-        private static int CoerceCaretIndex(int value, string? text)
+        internal static int CoerceCaretIndex(AvaloniaObject sender, int value)
         {
+            var text = sender.GetValue(TextProperty); // method also used by TextPresenter and SelectableTextBlock
+
             if (text == null)
             {
                 return 0;
@@ -1619,10 +1622,7 @@ namespace Avalonia.Controls
         /// <summary>
         /// Clears the text in the TextBox
         /// </summary>
-        public void Clear()
-        {
-            Text = string.Empty;
-        }
+        public void Clear() => SetCurrentValue(TextProperty, string.Empty);
 
         private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting)
         {
@@ -1645,7 +1645,7 @@ namespace Avalonia.Controls
                         LogicalDirection.Forward :
                         LogicalDirection.Backward);
 
-                    SelectionEnd = _presenter.CaretIndex;
+                    SetCurrentValue(SelectionEndProperty, _presenter.CaretIndex);
                 }
                 else
                 {
@@ -1662,7 +1662,7 @@ namespace Avalonia.Controls
                             LogicalDirection.Backward);
                     }
 
-                    CaretIndex = _presenter.CaretIndex;
+                    SetCurrentValue(CaretIndexProperty, _presenter.CaretIndex);
                 }
             }
             else
@@ -1678,17 +1678,17 @@ namespace Avalonia.Controls
                     offset = StringUtils.PreviousWord(text, selectionEnd) - selectionEnd;
                 }
 
-                SelectionEnd += offset;
+                SetCurrentValue(SelectionEndProperty, SelectionEnd + offset);
 
                 _presenter.MoveCaretToTextPosition(SelectionEnd);
 
                 if (!isSelecting)
                 {
-                    CaretIndex = SelectionEnd;
+                    SetCurrentValue(CaretIndexProperty, SelectionEnd);
                 }
                 else
                 {
-                    SelectionStart = selectionStart;
+                    SetCurrentValue(SelectionStartProperty, selectionStart);
                 }
             }
         }
@@ -1747,36 +1747,45 @@ namespace Avalonia.Controls
         /// </summary>
         public void SelectAll()
         {
-            SelectionStart = 0;
-            SelectionEnd = Text?.Length ?? 0;
+            SetCurrentValue(SelectionStartProperty, 0);
+            SetCurrentValue(SelectionEndProperty, Text?.Length ?? 0);
         }
 
-        internal bool DeleteSelection(bool raiseTextChanged = true)
+        private (int start, int end) GetSelectionRange()
+        {
+            var selectionStart = SelectionStart;
+            var selectionEnd = SelectionEnd;
+
+            return (Math.Min(selectionStart, selectionEnd), Math.Max(selectionStart, selectionEnd));
+        }
+
+        internal bool DeleteSelection()
         {
             if (IsReadOnly)
                 return true;
 
-            var selectionStart = SelectionStart;
-            var selectionEnd = SelectionEnd;
+            var (start, end) = GetSelectionRange();
 
-            if (selectionStart != selectionEnd)
+            if (start != end)
             {
-                var start = Math.Min(selectionStart, selectionEnd);
-                var end = Math.Max(selectionStart, selectionEnd);
                 var text = Text!;
+                var textBuilder = StringBuilderCache.Acquire(text.Length);
 
-                SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged);
+                textBuilder.Append(text);
+                textBuilder.Remove(start, end - start);
+
+                SetCurrentValue(TextProperty, textBuilder.ToString());
 
                 _presenter?.MoveCaretToTextPosition(start);
 
-                CaretIndex = start;
+                SetCurrentValue(CaretIndexProperty, start);
 
                 ClearSelection();
 
                 return true;
             }
 
-            CaretIndex = SelectionStart;
+            SetCurrentValue(CaretIndexProperty, SelectionStart);
 
             return false;
         }
@@ -1826,46 +1835,30 @@ namespace Avalonia.Controls
             }, DispatcherPriority.Normal);
         }
 
-        private void SetTextInternal(string value, bool raiseTextChanged = true)
-        {
-            if (raiseTextChanged)
-            {
-                bool textChanged = SetAndRaise(TextProperty, ref _text, value);
-
-                if (textChanged)
-                {
-                    RaiseTextChangeEvents();
-                }
-            }
-            else
-            {
-                _text = value;
-            }
-        }
-
         private void SetSelectionForControlBackspace()
         {
             var selectionStart = CaretIndex;
 
             MoveHorizontal(-1, true, false);
 
-            SelectionStart = selectionStart;
+            SetCurrentValue(SelectionStartProperty, selectionStart);
         }
 
         private void SetSelectionForControlDelete()
         {
-            if (_text == null || _presenter == null)
+            var textLength = Text?.Length ?? 0;
+            if (_presenter == null || textLength == 0)
             {
                 return;
             }
 
-            SelectionStart = CaretIndex;
+            SetCurrentValue(SelectionStartProperty, CaretIndex);
 
             MoveHorizontal(1, true, true);
 
-            if (SelectionEnd < _text.Length && _text[SelectionEnd] == ' ')
+            if (SelectionEnd < textLength && Text![SelectionEnd] == ' ')
             {
-                SelectionEnd++;
+                SetCurrentValue(SelectionEndProperty, SelectionEnd + 1);
             }
         }
 
@@ -1881,8 +1874,8 @@ namespace Avalonia.Controls
             get => new UndoRedoState(Text, CaretIndex);
             set
             {
-                Text = value.Text;
-                CaretIndex = value.CaretPosition;
+                SetCurrentValue(TextProperty, value.Text);
+                SetCurrentValue(CaretIndexProperty, value.CaretPosition);
                 ClearSelection();
             }
         }

+ 11 - 8
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@@ -106,7 +106,7 @@ namespace Avalonia.Controls
         {
             if (_presenter != null && _textEditable != null)
             {
-                _presenter.CompositionRegion = new TextRange(_textEditable.CompositionStart, _textEditable.CompositionEnd);
+                _presenter.SetCurrentValue(TextPresenter.CompositionRegionProperty, new TextRange(_textEditable.CompositionStart, _textEditable.CompositionEnd));
             }
         }
 
@@ -177,9 +177,9 @@ namespace Avalonia.Controls
 
             var text = GetText(preeditText);
 
-            _presenter._text = text;
+            _presenter.SetCurrentValue(TextPresenter.TextProperty, text);
 
-            _presenter.PreeditText = preeditText;
+            _presenter.SetCurrentValue(TextPresenter.PreeditTextProperty, preeditText);
 
             _presenter.UpdateCaret(new CharacterHit(_compositionStart + (preeditText != null ? preeditText.Length : 0)), false);
 
@@ -201,9 +201,12 @@ namespace Avalonia.Controls
                 return preeditText;
             }
 
-            var text = _presenterText.Substring(0, _compositionStart) + preeditText + _presenterText.Substring(_compositionStart);
+            var sb = StringBuilderCache.Acquire(_presenterText.Length + preeditText.Length);
 
-            return text;
+            sb.Append(_presenterText);
+            sb.Insert(_compositionStart, preeditText);
+
+            return StringBuilderCache.GetStringAndRelease(sb);
         }
 
         public void SetComposingRegion(TextRange? region)
@@ -213,7 +216,7 @@ namespace Avalonia.Controls
                 return;
             }
 
-            _presenter.CompositionRegion = region;
+            _presenter.SetCurrentValue(TextPresenter.CompositionRegionProperty, region);
         }
 
         public void SelectInSurroundingText(int start, int end)
@@ -252,9 +255,9 @@ namespace Avalonia.Controls
 
             if (_presenter != null)
             {
-                _presenter.PreeditText = null;
+                _presenter.ClearValue(TextPresenter.PreeditTextProperty);
 
-                _presenter.CompositionRegion = null;
+                _presenter.ClearValue(TextPresenter.CompositionRegionProperty);
 
                 _presenter.CaretBoundsChanged -= OnCaretBoundsChanged;
             }

+ 0 - 1
src/Avalonia.Controls/TopLevel.cs

@@ -15,7 +15,6 @@ using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Platform.Storage;
-using Avalonia.Reactive;
 using Avalonia.Rendering;
 using Avalonia.Styling;
 using Avalonia.Utilities;

+ 2 - 2
src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs

@@ -144,13 +144,13 @@ namespace Avalonia.Controls.Utils
         {
             get
             {
-                return SelectorControl?.Items;
+                return SelectorControl?.ItemsSource;
             }
             set
             {
                 if (SelectorControl != null)
                 {
-                    SelectorControl.Items = value;
+                    SelectorControl.ItemsSource = value;
                 }
             }
         }

+ 3 - 1
src/Avalonia.Controls/Utils/UndoRedoHelper.cs

@@ -4,6 +4,8 @@ namespace Avalonia.Controls.Utils
 {
     class UndoRedoHelper<TState>
     {
+        public const int DefaultUndoLimit = 10;
+
         private readonly IUndoRedoHost _host;
 
         public interface IUndoRedoHost
@@ -23,7 +25,7 @@ namespace Avalonia.Controls.Utils
         /// Maximum number of states this helper can store for undo/redo.
         /// If -1, no limit is imposed.
         /// </summary>
-        public int Limit { get; set; } = 10;
+        public int Limit { get; set; } = DefaultUndoLimit;
 
         public bool CanUndo => _currentNode?.Previous != null;
 

+ 3 - 17
src/Avalonia.Controls/VirtualizingPanel.cs

@@ -34,7 +34,8 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets the items to display.
         /// </summary>
-        protected IReadOnlyList<object?> Items => ItemsControl?.ItemsView ?? ItemsSourceView.Empty;
+        protected IReadOnlyList<object?> Items => (IReadOnlyList<object?>?)ItemsControl?.ItemsView ?? 
+            Array.Empty<object?>();
 
         /// <summary>
         /// Gets the <see cref="ItemsControl"/> that the panel is displaying items for.
@@ -192,17 +193,13 @@ namespace Avalonia.Controls
                 throw new InvalidOperationException("The VirtualizingPanel is already attached to an ItemsControl");
 
             ItemsControl = itemsControl;
-            ItemsControl.PropertyChanged += OnItemsControlPropertyChanged;
             ItemsControl.ItemsView.PostCollectionChanged += OnItemsControlItemsChanged;
         }
 
         internal void Detach()
         {
             var itemsControl = EnsureItemsControl();
-
-            itemsControl.PropertyChanged -= OnItemsControlPropertyChanged;
             itemsControl.ItemsView.PostCollectionChanged -= OnItemsControlItemsChanged;
-
             ItemsControl = null;
             Children.Clear();
         }
@@ -216,20 +213,9 @@ namespace Avalonia.Controls
             return ItemsControl;
         }
 
-        private protected virtual void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
-        {
-            if (e.Property == ItemsControl.ItemsViewProperty)
-            {
-                var (oldValue, newValue) = e.GetOldAndNewValue<ItemsSourceView>();
-                oldValue.PostCollectionChanged -= OnItemsControlItemsChanged;
-                Refresh();
-                newValue.PostCollectionChanged += OnItemsControlItemsChanged;
-            }
-        }
-
         private void OnItemsControlItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
         {
-            OnItemsChanged(_itemsControl?.ItemsView ?? ItemsSourceView.Empty, e);
+            OnItemsChanged(Items, e);
         }
 
         [DoesNotReturn]

+ 3 - 2
src/Avalonia.Controls/VirtualizingStackPanel.cs

@@ -459,7 +459,8 @@ namespace Avalonia.Controls
 
             while (c is not null)
             {
-                if (!c.Bounds.IsDefault && c.TransformToVisual(this) is Matrix transform)
+                if ((c.Bounds.Width != 0 || c.Bounds.Height != 0) &&
+                    c.TransformToVisual(this) is Matrix transform)
                 {
                     viewport = new Rect(0, 0, c.Bounds.Width, c.Bounds.Height)
                         .TransformToAABB(transform);
@@ -1078,7 +1079,7 @@ namespace Avalonia.Controls
                     // elements after the insertion point.
                     var elementCount = _elements.Count;
                     var start = Math.Max(realizedIndex, 0);
-                    var newIndex = first + count;
+                    var newIndex = realizedIndex + count;
 
                     for (var i = start; i < elementCount; ++i)
                     {

+ 12 - 17
src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs

@@ -7,23 +7,18 @@ namespace Avalonia.Diagnostics.Controls
 {
     internal class FilterTextBox : TextBox, IStyleable
     {
-        public static readonly DirectProperty<FilterTextBox, bool> UseRegexFilterProperty =
-            AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseRegexFilter),
-                o => o.UseRegexFilter, (o, v) => o.UseRegexFilter = v,
+        public static readonly StyledProperty<bool> UseRegexFilterProperty =
+            AvaloniaProperty.Register<FilterTextBox, bool>(nameof(UseRegexFilter),
                 defaultBindingMode: BindingMode.TwoWay);
 
-        public static readonly DirectProperty<FilterTextBox, bool> UseCaseSensitiveFilterProperty =
-            AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseCaseSensitiveFilter),
-                o => o.UseCaseSensitiveFilter, (o, v) => o.UseCaseSensitiveFilter = v,
+        public static readonly StyledProperty<bool> UseCaseSensitiveFilterProperty =
+            AvaloniaProperty.Register<FilterTextBox, bool>(nameof(UseCaseSensitiveFilter),
                 defaultBindingMode: BindingMode.TwoWay);
 
-        public static readonly DirectProperty<FilterTextBox, bool> UseWholeWordFilterProperty =
-            AvaloniaProperty.RegisterDirect<FilterTextBox, bool>(nameof(UseWholeWordFilter),
-                o => o.UseWholeWordFilter, (o, v) => o.UseWholeWordFilter = v,
+        public static readonly StyledProperty<bool> UseWholeWordFilterProperty =
+            AvaloniaProperty.Register<FilterTextBox, bool>(nameof(UseWholeWordFilter),
                 defaultBindingMode: BindingMode.TwoWay);
 
-        private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter;
-
         public FilterTextBox()
         {
             Classes.Add("filter-text-box");
@@ -31,20 +26,20 @@ namespace Avalonia.Diagnostics.Controls
 
         public bool UseRegexFilter
         {
-            get => _useRegexFilter;
-            set => SetAndRaise(UseRegexFilterProperty, ref _useRegexFilter, value);
+            get => GetValue(UseRegexFilterProperty);
+            set => SetValue(UseRegexFilterProperty, value);
         }
 
         public bool UseCaseSensitiveFilter
         {
-            get => _useCaseSensitiveFilter;
-            set => SetAndRaise(UseCaseSensitiveFilterProperty, ref _useCaseSensitiveFilter, value);
+            get => GetValue(UseCaseSensitiveFilterProperty);
+            set => SetValue(UseCaseSensitiveFilterProperty,value);
         }
 
         public bool UseWholeWordFilter
         {
-            get => _useWholeWordFilter;
-            set => SetAndRaise(UseWholeWordFilterProperty, ref _useWholeWordFilter, value);
+            get => GetValue(UseWholeWordFilterProperty);
+            set => SetValue(UseWholeWordFilterProperty, value);
         }
 
         Type IStyleable.StyleKey => typeof(TextBox);

+ 1 - 1
src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml

@@ -40,7 +40,7 @@
              BorderBrush="{DynamicResource ThemeControlMidBrush}"
              BorderThickness="0,0,0,1"
              FontFamily="/Assets/Fonts/SourceSansPro-Regular.ttf"
-             Items="{Binding History}">
+             ItemsSource="{Binding History}">
       <ListBox.ItemTemplate>
         <DataTemplate>
           <StackPanel Orientation="Vertical">

+ 3 - 3
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@@ -124,7 +124,7 @@
         </Grid>
 
         <ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled">
-          <ItemsControl Items="{Binding AppliedStyles}" >
+          <ItemsControl ItemsSource="{Binding AppliedStyles}" >
             <ItemsControl.ItemTemplate>
               <DataTemplate>
                 <Border BorderThickness="0,0,0,1" BorderBrush="#6C6C6C" Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}">
@@ -142,7 +142,7 @@
                       <TextBlock Grid.Row="0" Text="{Binding Name}" />
                     </Expander.Header>
 
-                    <ItemsControl Margin="20,0,0,0" Grid.Row="1" Items="{Binding Setters}">
+                    <ItemsControl Margin="20,0,0,0" Grid.Row="1" ItemsSource="{Binding Setters}">
                       
                       <ItemsControl.Styles>
                         <Style Selector="TextBlock.property-name">
@@ -253,7 +253,7 @@
         </ScrollViewer>
 
         <Expander Header="Pseudo Classes" Grid.Row="2">
-          <ItemsControl Items="{Binding PseudoClasses}">
+          <ItemsControl ItemsSource="{Binding PseudoClasses}">
             <ItemsControl.ItemsPanel>
               <ItemsPanelTemplate>
                 <WrapPanel />

+ 3 - 3
src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml

@@ -45,7 +45,7 @@
                               UseWholeWordFilter="{Binding UseWholeWordFilter}"
                               UseRegexFilter="{Binding UseRegexFilter}" />
 
-      <TreeView Grid.Row="1" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" >
+      <TreeView Grid.Row="1" ItemsSource="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}" >
         <TreeView.DataTemplates>
           <TreeDataTemplate DataType="vm:EventTreeNodeBase"
                             ItemsSource="{Binding Children}">
@@ -71,7 +71,7 @@
 
     <Grid RowDefinitions="*,4,2*,Auto" Grid.Column="2">
 
-      <ListBox Name="EventsList" Items="{Binding RecordedEvents}"
+      <ListBox Name="EventsList" ItemsSource="{Binding RecordedEvents}"
                SelectedItem="{Binding SelectedEvent, Mode=TwoWay}">
 
         <ListBox.ItemTemplate>
@@ -108,7 +108,7 @@
       <DockPanel Grid.Row="2" LastChildFill="True">
         <TextBlock DockPanel.Dock="Top" FontSize="16" Text="Event chain:" />
 
-        <ListBox Items="{Binding SelectedEvent.EventChain}">
+        <ListBox ItemsSource="{Binding SelectedEvent.EventChain}">
           <ListBox.ItemTemplate>
             <DataTemplate>
               <ListBoxItem Classes.handled="{Binding Handled}">

+ 1 - 1
src/Avalonia.Diagnostics/Diagnostics/Views/PropertyValueEditorView.cs

@@ -221,7 +221,7 @@ namespace Avalonia.Diagnostics.Views
                 return CreateControl<ComboBox>(
                     SelectingItemsControl.SelectedItemProperty, init: c =>
                     {
-                        c.Items = Enum.GetValues(propertyType);
+                        c.ItemsSource = Enum.GetValues(propertyType);
                     });
 
             var tb = CreateControl<CommitTextBox>(

+ 1 - 1
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml

@@ -6,7 +6,7 @@
   <Grid ColumnDefinitions="0.35*,4,0.65*">
     <TreeView Name="tree"
               BorderThickness="0"
-              Items="{Binding Nodes}"
+              ItemsSource="{Binding Nodes}"
               SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
       <TreeView.DataTemplates>
         <TreeDataTemplate DataType="vm:TreeNode"

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