Browse Source

Merge branch 'master' into pr/4658

pull/4658/head
Steven Kirk 6 years ago
parent
commit
93ccafc97c
  1. 54
      Avalonia.sln
  2. 3
      Avalonia.v3.ncrunchsolution
  3. 2
      build/NetFX.props
  4. 4
      build/SkiaSharp.props
  5. 5
      samples/ControlCatalog/MainView.xaml
  6. 50
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  7. 57
      samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
  8. 8
      samples/Sandbox/App.axaml
  9. 22
      samples/Sandbox/App.axaml.cs
  10. 4
      samples/Sandbox/MainWindow.axaml
  11. 20
      samples/Sandbox/MainWindow.axaml.cs
  12. 17
      samples/Sandbox/Program.cs
  13. 18
      samples/Sandbox/Sandbox.csproj
  14. 8
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  15. 1
      src/Avalonia.Controls/ListBox.cs
  16. 336
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  17. 273
      src/Avalonia.Controls/Selection/InternalSelectionModel.cs
  18. 106
      src/Avalonia.Controls/Selection/SelectionModel.cs
  19. 55
      src/Avalonia.Controls/TabControl.cs
  20. 283
      src/Avalonia.Controls/Utils/SelectedItemsSync.cs
  21. 6
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs
  22. 12
      src/Avalonia.Input/KeyGesture.cs
  23. 4
      src/Avalonia.Input/PointerEventArgs.cs
  24. 9
      src/Avalonia.OpenGL/Angle/AngleEglInterface.cs
  25. 6
      src/Avalonia.OpenGL/AngleOptions.cs
  26. 38
      src/Avalonia.OpenGL/EglDisplay.cs
  27. 2
      src/Avalonia.X11/Glx/GlxDisplay.cs
  28. 4
      src/Avalonia.X11/Glx/GlxPlatformFeature.cs
  29. 4
      src/Avalonia.X11/X11Platform.cs
  30. 447
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  31. 254
      tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs
  32. 56
      tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs
  33. 56
      tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs
  34. 84
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  35. 278
      tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs

54
Avalonia.sln

@ -224,6 +224,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events.UnitTests", "tests\Avalonia.ReactiveUI.Events.UnitTests\Avalonia.ReactiveUI.Events.UnitTests.csproj", "{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}"
EndProject
Global Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
@ -2038,6 +2042,54 @@ Global
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.Build.0 = Release|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.ActiveCfg = Release|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.Build.0 = Release|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|Any CPU.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhone.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhone.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhone.Build.0 = Debug|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|Any CPU.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.Build.0 = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -2096,6 +2148,8 @@ Global
{351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
{909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

3
Avalonia.v3.ncrunchsolution

@ -6,6 +6,9 @@
<Value>src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.dll</Value> <Value>src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.dll</Value>
</AdditionalFilesToIncludeForSolution> </AdditionalFilesToIncludeForSolution>
<AllowParallelTestExecution>True</AllowParallelTestExecution> <AllowParallelTestExecution>True</AllowParallelTestExecution>
<CustomBuildProperties>
<Value>RunApiCompat = false</Value>
</CustomBuildProperties>
<ProjectConfigStoragePathRelativeToSolutionDir>.ncrunch</ProjectConfigStoragePathRelativeToSolutionDir> <ProjectConfigStoragePathRelativeToSolutionDir>.ncrunch</ProjectConfigStoragePathRelativeToSolutionDir>
<SolutionConfigured>True</SolutionConfigured> <SolutionConfigured>True</SolutionConfigured>
</Settings> </Settings>

2
build/NetFX.props

@ -1,7 +1,7 @@
<Project> <Project>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0-preview.2" PrivateAssets="All" /> <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

4
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup> <ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.80.2-preview.33" /> <PackageReference Include="SkiaSharp" Version="2.80.2" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.80.2-preview.33" /> <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.80.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

5
samples/ControlCatalog/MainView.xaml

@ -45,7 +45,10 @@
<pages:ItemsRepeaterPage/> <pages:ItemsRepeaterPage/>
</TabItem> </TabItem>
<TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem> <TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem>
<TabItem Header="ListBox"><pages:ListBoxPage/></TabItem> <TabItem Header="ListBox"
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<pages:ListBoxPage/>
</TabItem>
<TabItem Header="Menu"><pages:MenuPage/></TabItem> <TabItem Header="Menu"><pages:MenuPage/></TabItem>
<TabItem Header="Notifications"><pages:NotificationsPage/></TabItem> <TabItem Header="Notifications"><pages:NotificationsPage/></TabItem>
<TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem> <TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem>

50
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -1,35 +1,25 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ListBoxPage"> x:Class="ControlCatalog.Pages.ListBoxPage">
<StackPanel Orientation="Vertical" Spacing="4"> <DockPanel>
<TextBlock Classes="h1">ListBox</TextBlock> <StackPanel DockPanel.Dock="Top" Margin="4">
<TextBlock Classes="h2">Hosts a collection of ListBoxItem.</TextBlock> <TextBlock Classes="h1">ListBox</TextBlock>
<TextBlock Classes="h2">Hosts a collection of ListBoxItem.</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<ListBox Items="{Binding Items}"
Selection="{Binding Selection}"
AutoScrollToSelectedItem="True"
SelectionMode="{Binding SelectionMode}"
Width="250"
Height="350"/>
<Button Command="{Binding AddItemCommand}">Add</Button>
<Button Command="{Binding RemoveItemCommand}">Remove</Button>
<Button Command="{Binding SelectRandomItemCommand}">Select Random Item</Button>
<ComboBox SelectedIndex="{Binding SelectionMode, Mode=TwoWay}">
<ComboBoxItem>Single</ComboBoxItem>
<ComboBoxItem>Multiple</ComboBoxItem>
<ComboBoxItem>Toggle</ComboBoxItem>
<ComboBoxItem>AlwaysSelected</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel> </StackPanel>
</StackPanel> <StackPanel DockPanel.Dock="Right" Margin="4">
<CheckBox IsChecked="{Binding Multiple}">Multiple</CheckBox>
<CheckBox IsChecked="{Binding Toggle}">Toggle</CheckBox>
<CheckBox IsChecked="{Binding AlwaysSelected}">AlwaysSelected</CheckBox>
<CheckBox IsChecked="{Binding AutoScrollToSelectedItem}">AutoScrollToSelectedItem</CheckBox>
</StackPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="4">
<Button Command="{Binding AddItemCommand}">Add</Button>
<Button Command="{Binding RemoveItemCommand}">Remove</Button>
<Button Command="{Binding SelectRandomItemCommand}">Select Random Item</Button>
</StackPanel>
<ListBox Items="{Binding Items}"
Selection="{Binding Selection}"
AutoScrollToSelectedItem="{Binding AutoScrollToSelectedItem}"
SelectionMode="{Binding SelectionMode}"/>
</DockPanel>
</UserControl> </UserControl>

57
samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs

@ -10,22 +10,39 @@ namespace ControlCatalog.ViewModels
{ {
public class ListBoxPageViewModel : ReactiveObject public class ListBoxPageViewModel : ReactiveObject
{ {
private bool _multiple;
private bool _toggle;
private bool _alwaysSelected;
private bool _autoScrollToSelectedItem = true;
private int _counter; private int _counter;
private SelectionMode _selectionMode; private ObservableAsPropertyHelper<SelectionMode> _selectionMode;
public ListBoxPageViewModel() public ListBoxPageViewModel()
{ {
Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem())); Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
Selection = new SelectionModel<string>(); Selection = new SelectionModel<string>();
Selection.Select(1); Selection.Select(1);
_selectionMode = this.WhenAnyValue(
x => x.Multiple,
x => x.Toggle,
x => x.AlwaysSelected,
(m, t, a) =>
(m ? SelectionMode.Multiple : 0) |
(t ? SelectionMode.Toggle : 0) |
(a ? SelectionMode.AlwaysSelected : 0))
.ToProperty(this, x => x.SelectionMode);
AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem())); AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
RemoveItemCommand = ReactiveCommand.Create(() => RemoveItemCommand = ReactiveCommand.Create(() =>
{ {
while (Selection.Count > 0) var items = Selection.SelectedItems.ToList();
foreach (var item in items)
{ {
Items.Remove(Selection.SelectedItems.First()); Items.Remove(item);
} }
}); });
@ -42,25 +59,37 @@ namespace ControlCatalog.ViewModels
} }
public ObservableCollection<string> Items { get; } public ObservableCollection<string> Items { get; }
public SelectionModel<string> Selection { get; } public SelectionModel<string> Selection { get; }
public SelectionMode SelectionMode => _selectionMode.Value;
public ReactiveCommand<Unit, Unit> AddItemCommand { get; } public bool Multiple
{
get => _multiple;
set => this.RaiseAndSetIfChanged(ref _multiple, value);
}
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; } public bool Toggle
{
get => _toggle;
set => this.RaiseAndSetIfChanged(ref _toggle, value);
}
public ReactiveCommand<Unit, Unit> SelectRandomItemCommand { get; } public bool AlwaysSelected
{
get => _alwaysSelected;
set => this.RaiseAndSetIfChanged(ref _alwaysSelected, value);
}
public SelectionMode SelectionMode public bool AutoScrollToSelectedItem
{ {
get => _selectionMode; get => _autoScrollToSelectedItem;
set set => this.RaiseAndSetIfChanged(ref _autoScrollToSelectedItem, value);
{
Selection.Clear();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
} }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public ReactiveCommand<Unit, Unit> SelectRandomItemCommand { get; }
private string GenerateItem() => $"Item {_counter++.ToString()}"; private string GenerateItem() => $"Item {_counter++.ToString()}";
} }
} }

8
samples/Sandbox/App.axaml

@ -0,0 +1,8 @@
<Application
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sandbox.App">
<Application.Styles>
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Accents/FluentDark.xaml"/>
</Application.Styles>
</Application>

22
samples/Sandbox/App.axaml.cs

@ -0,0 +1,22 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace Sandbox
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{
desktopLifetime.MainWindow = new MainWindow();
}
}
}
}

4
samples/Sandbox/MainWindow.axaml

@ -0,0 +1,4 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
x:Class="Sandbox.MainWindow">
</Window>

20
samples/Sandbox/MainWindow.axaml.cs

@ -0,0 +1,20 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Sandbox
{
public class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
this.AttachDevTools();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

17
samples/Sandbox/Program.cs

@ -0,0 +1,17 @@
using Avalonia;
using Avalonia.ReactiveUI;
namespace Sandbox
{
public class Program
{
static void Main(string[] args)
{
AppBuilder.Configure<App>()
.UsePlatformDetect()
.UseReactiveUI()
.LogToDebug()
.StartWithClassicDesktopLifetime(args);
}
}
}

18
samples/Sandbox/Sandbox.csproj

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
</ItemGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
<Import Project="..\..\build\BuildTargets.targets" />
</Project>

8
src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs

@ -49,8 +49,12 @@ namespace Avalonia.Controls
{ {
if(_binding is Avalonia.Data.Binding binding) if(_binding is Avalonia.Data.Binding binding)
{ {
// Force the TwoWay binding mode if there is a Path present. TwoWay binding requires a Path. if (binding.Mode == BindingMode.OneWayToSource)
if (!String.IsNullOrEmpty(binding.Path)) {
throw new InvalidOperationException("DataGridColumn doesn't support BindingMode.OneWayToSource. Use BindingMode.TwoWay instead.");
}
if (!String.IsNullOrEmpty(binding.Path) && binding.Mode == BindingMode.Default)
{ {
binding.Mode = BindingMode.TwoWay; binding.Mode = BindingMode.TwoWay;
} }

1
src/Avalonia.Controls/ListBox.cs

@ -163,6 +163,7 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
base.OnApplyTemplate(e);
Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer"); Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
} }
} }

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

@ -6,7 +6,6 @@ using System.ComponentModel;
using System.Linq; using System.Linq;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Selection; using Avalonia.Controls.Selection;
using Avalonia.Controls.Utils;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
@ -70,8 +69,8 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Defines the <see cref="SelectedItems"/> property. /// Defines the <see cref="SelectedItems"/> property.
/// </summary> /// </summary>
protected static readonly DirectProperty<SelectingItemsControl, IList> SelectedItemsProperty = protected static readonly DirectProperty<SelectingItemsControl, IList?> SelectedItemsProperty =
AvaloniaProperty.RegisterDirect<SelectingItemsControl, IList>( AvaloniaProperty.RegisterDirect<SelectingItemsControl, IList?>(
nameof(SelectedItems), nameof(SelectedItems),
o => o.SelectedItems, o => o.SelectedItems,
(o, v) => o.SelectedItems = v); (o, v) => o.SelectedItems = v);
@ -111,12 +110,13 @@ namespace Avalonia.Controls.Primitives
RoutingStrategies.Bubble); RoutingStrategies.Bubble);
private static readonly IList Empty = Array.Empty<object>(); private static readonly IList Empty = Array.Empty<object>();
private SelectedItemsSync? _selectedItemsSync;
private ISelectionModel? _selection; private ISelectionModel? _selection;
private int _oldSelectedIndex; private int _oldSelectedIndex;
private object? _oldSelectedItem; private object? _oldSelectedItem;
private int _initializing; private IList? _oldSelectedItems;
private bool _ignoreContainerSelectionChanged; private bool _ignoreContainerSelectionChanged;
private UpdateState? _updateState;
private bool _hasScrolledToSelectedItem;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="SelectingItemsControl"/> class. /// Initializes static members of the <see cref="SelectingItemsControl"/> class.
@ -149,8 +149,27 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public int SelectedIndex public int SelectedIndex
{ {
get => Selection.SelectedIndex; get
set => Selection.SelectedIndex = value; {
// When a Begin/EndInit/DataContext update is in place we return the value to be
// updated here, even though it's not yet active and the property changed notification
// has not yet been raised. If we don't do this then the old value will be written back
// to the source when two-way bound, and the update value will be lost.
return _updateState?.SelectedIndex.HasValue == true ?
_updateState.SelectedIndex.Value :
Selection.SelectedIndex;
}
set
{
if (_updateState is object)
{
_updateState.SelectedIndex = value;
}
else
{
Selection.SelectedIndex = value;
}
}
} }
/// <summary> /// <summary>
@ -158,17 +177,67 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public object? SelectedItem public object? SelectedItem
{ {
get => Selection.SelectedItem; get
set => Selection.SelectedItem = value; {
// See SelectedIndex setter for more information.
return _updateState?.SelectedItem.HasValue == true ?
_updateState.SelectedItem.Value :
Selection.SelectedItem;
}
set
{
if (_updateState is object)
{
_updateState.SelectedItem = value;
}
else
{
Selection.SelectedItem = value;
}
}
} }
/// <summary> /// <summary>
/// Gets or sets the selected items. /// Gets or sets the selected items.
/// </summary> /// </summary>
protected IList SelectedItems /// <remarks>
/// By default returns a collection that can be modified in order to manipulate the control
/// selection, however this property will return null if <see cref="Selection"/> is
/// re-assigned; you should only use _either_ Selection or SelectedItems.
/// </remarks>
protected IList? SelectedItems
{ {
get => SelectedItemsSync.SelectedItems; get
set => SelectedItemsSync.SelectedItems = value; {
// See SelectedIndex setter for more information.
if (_updateState?.SelectedItems.HasValue == true)
{
return _updateState.SelectedItems.Value;
}
else if (Selection is InternalSelectionModel ism)
{
var result = ism.WritableSelectedItems;
_oldSelectedItems = result;
return result;
}
return null;
}
set
{
if (_updateState is object)
{
_updateState.SelectedItems = new Optional<IList?>(value);
}
else if (Selection is InternalSelectionModel i)
{
i.WritableSelectedItems = value;
}
else
{
throw new InvalidOperationException("Cannot set both Selection and SelectedItems.");
}
}
} }
/// <summary> /// <summary>
@ -178,19 +247,30 @@ namespace Avalonia.Controls.Primitives
{ {
get get
{ {
if (_selection is null) if (_updateState?.Selection.HasValue == true)
{ {
_selection = CreateDefaultSelectionModel(); return _updateState.Selection.Value;
InitializeSelectionModel(_selection);
} }
else
{
if (_selection is null)
{
_selection = CreateDefaultSelectionModel();
InitializeSelectionModel(_selection);
}
return _selection; return _selection;
}
} }
set set
{ {
value ??= CreateDefaultSelectionModel(); value ??= CreateDefaultSelectionModel();
if (_selection != value) if (_updateState is object)
{
_updateState.Selection = new Optional<ISelectionModel>(value);
}
else if (_selection != value)
{ {
if (value.Source != null && value.Source != Items) if (value.Source != null && value.Source != Items)
{ {
@ -212,6 +292,15 @@ namespace Avalonia.Controls.Primitives
} }
InitializeSelectionModel(_selection); InitializeSelectionModel(_selection);
if (_oldSelectedItems != SelectedItems)
{
RaisePropertyChanged(
SelectedItemsProperty,
new Optional<IList?>(_oldSelectedItems),
new BindingValue<IList?>(SelectedItems));
_oldSelectedItems = SelectedItems;
}
} }
} }
} }
@ -234,20 +323,18 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0; protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0;
private SelectedItemsSync SelectedItemsSync => _selectedItemsSync ??= new SelectedItemsSync(Selection);
/// <inheritdoc/> /// <inheritdoc/>
public override void BeginInit() public override void BeginInit()
{ {
base.BeginInit(); base.BeginInit();
++_initializing; BeginUpdating();
} }
/// <inheritdoc/> /// <inheritdoc/>
public override void EndInit() public override void EndInit()
{ {
base.EndInit(); base.EndInit();
--_initializing; EndUpdating();
} }
/// <summary> /// <summary>
@ -295,6 +382,28 @@ namespace Avalonia.Controls.Primitives
} }
} }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
AutoScrollToSelectedItemIfNecessary();
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
void ExecuteScrollWhenLayoutUpdated(object sender, EventArgs e)
{
LayoutUpdated -= ExecuteScrollWhenLayoutUpdated;
AutoScrollToSelectedItemIfNecessary();
}
if (AutoScrollToSelectedItem)
{
LayoutUpdated += ExecuteScrollWhenLayoutUpdated;
}
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnContainersMaterialized(ItemContainerEventArgs e) protected override void OnContainersMaterialized(ItemContainerEventArgs e)
{ {
@ -351,30 +460,14 @@ namespace Avalonia.Controls.Primitives
protected override void OnDataContextBeginUpdate() protected override void OnDataContextBeginUpdate()
{ {
base.OnDataContextBeginUpdate(); base.OnDataContextBeginUpdate();
++_initializing; BeginUpdating();
if (_selection is object)
{
_selection.Source = null;
}
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnDataContextEndUpdate() protected override void OnDataContextEndUpdate()
{ {
base.OnDataContextEndUpdate(); base.OnDataContextEndUpdate();
--_initializing; EndUpdating();
if (_selection is object && _initializing == 0)
{
_selection.Source = Items;
if (Items is null)
{
_selection.Clear();
_selectedItemsSync?.SelectedItems?.Clear();
}
}
} }
protected override void OnInitialized() protected override void OnInitialized()
@ -411,9 +504,11 @@ namespace Avalonia.Controls.Primitives
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == ItemsProperty && if (change.Property == AutoScrollToSelectedItemProperty)
_initializing == 0 && {
_selection is object) AutoScrollToSelectedItemIfNecessary();
}
if (change.Property == ItemsProperty && _updateState is null && _selection is object)
{ {
var newValue = change.NewValue.GetValueOrDefault<IEnumerable>(); var newValue = change.NewValue.GetValueOrDefault<IEnumerable>();
_selection.Source = newValue; _selection.Source = newValue;
@ -601,23 +696,30 @@ namespace Avalonia.Controls.Primitives
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{ {
if (e.PropertyName == nameof(ISelectionModel.AnchorIndex) && AutoScrollToSelectedItem) if (e.PropertyName == nameof(ISelectionModel.AnchorIndex))
{ {
if (Selection.AnchorIndex > 0) _hasScrolledToSelectedItem = false;
{ AutoScrollToSelectedItemIfNecessary();
ScrollIntoView(Selection.AnchorIndex);
}
} }
else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex)) else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex) && _oldSelectedIndex != SelectedIndex)
{ {
RaisePropertyChanged(SelectedIndexProperty, _oldSelectedIndex, SelectedIndex); RaisePropertyChanged(SelectedIndexProperty, _oldSelectedIndex, SelectedIndex);
_oldSelectedIndex = SelectedIndex; _oldSelectedIndex = SelectedIndex;
} }
else if (e.PropertyName == nameof(ISelectionModel.SelectedItem)) else if (e.PropertyName == nameof(ISelectionModel.SelectedItem) && _oldSelectedItem != SelectedItem)
{ {
RaisePropertyChanged(SelectedItemProperty, _oldSelectedItem, SelectedItem); RaisePropertyChanged(SelectedItemProperty, _oldSelectedItem, SelectedItem);
_oldSelectedItem = SelectedItem; _oldSelectedItem = SelectedItem;
} }
else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) &&
_oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems)
{
RaisePropertyChanged(
SelectedItemsProperty,
new Optional<IList?>(_oldSelectedItems),
new BindingValue<IList?>(SelectedItems));
_oldSelectedItems = SelectedItems;
}
} }
/// <summary> /// <summary>
@ -674,6 +776,19 @@ namespace Avalonia.Controls.Primitives
} }
} }
private void AutoScrollToSelectedItemIfNecessary()
{
if (AutoScrollToSelectedItem &&
!_hasScrolledToSelectedItem &&
Presenter is object &&
Selection.AnchorIndex >= 0 &&
((IVisual)this).IsAttachedToVisualTree)
{
ScrollIntoView(Selection.AnchorIndex);
_hasScrolledToSelectedItem = true;
}
}
/// <summary> /// <summary>
/// Called when a container raises the <see cref="IsSelectedChangedEvent"/>. /// Called when a container raises the <see cref="IsSelectedChangedEvent"/>.
/// </summary> /// </summary>
@ -734,14 +849,6 @@ namespace Avalonia.Controls.Primitives
} }
} }
private void MarkContainersUnselected()
{
foreach (var container in ItemContainerGenerator.Containers)
{
MarkContainerSelected(container.ContainerControl, false);
}
}
/// <summary> /// <summary>
/// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>. /// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary> /// </summary>
@ -757,23 +864,6 @@ namespace Avalonia.Controls.Primitives
} }
} }
/// <summary>
/// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="selected">Whether the item should be selected or deselected.</param>
private int MarkItemSelected(object item, bool selected)
{
var index = IndexOf(Items, item);
if (index != -1)
{
MarkItemSelected(index, selected);
}
return index;
}
private void UpdateContainerSelection() private void UpdateContainerSelection()
{ {
if (Presenter?.Panel is IPanel panel) if (Presenter?.Panel is IPanel panel)
@ -789,7 +879,7 @@ namespace Avalonia.Controls.Primitives
private ISelectionModel CreateDefaultSelectionModel() private ISelectionModel CreateDefaultSelectionModel()
{ {
return new SelectionModel<object> return new InternalSelectionModel
{ {
SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
}; };
@ -797,7 +887,7 @@ namespace Avalonia.Controls.Primitives
private void InitializeSelectionModel(ISelectionModel model) private void InitializeSelectionModel(ISelectionModel model)
{ {
if (_initializing == 0) if (_updateState is null)
{ {
model.Source = Items; model.Source = Items;
} }
@ -825,9 +915,6 @@ namespace Avalonia.Controls.Primitives
UpdateContainerSelection(); UpdateContainerSelection();
_selectedItemsSync ??= new SelectedItemsSync(model);
_selectedItemsSync.SelectionModel = model;
if (SelectedIndex != -1) if (SelectedIndex != -1)
{ {
RaiseEvent(new SelectionChangedEventArgs( RaiseEvent(new SelectionChangedEventArgs(
@ -845,5 +932,96 @@ namespace Avalonia.Controls.Primitives
model.SelectionChanged -= OnSelectionModelSelectionChanged; model.SelectionChanged -= OnSelectionModelSelectionChanged;
} }
} }
private void BeginUpdating()
{
_updateState ??= new UpdateState();
_updateState.UpdateCount++;
}
private void EndUpdating()
{
if (_updateState is object && --_updateState.UpdateCount == 0)
{
var state = _updateState;
_updateState = null;
if (state.Selection.HasValue)
{
Selection = state.Selection.Value;
}
if (state.SelectedItems.HasValue)
{
SelectedItems = state.SelectedItems.Value;
}
Selection.Source = Items;
if (Items is null)
{
Selection.Clear();
}
if (state.SelectedIndex.HasValue)
{
SelectedIndex = state.SelectedIndex.Value;
}
else if (state.SelectedItem.HasValue)
{
SelectedItem = state.SelectedItem.Value;
}
}
}
// When in a BeginInit..EndInit block, or when the DataContext is updating, we need to
// defer changes to the selection model because we have no idea in which order properties
// will be set. Consider:
//
// - Both Items and SelectedItem are bound
// - The DataContext changes
// - The binding for SelectedItem updates first, producing an item
// - Items is searched to find the index of the new selected item
// - However Items isn't yet updated; the item is not found
// - SelectedIndex is incorrectly set to -1
//
// This logic cannot be encapsulated in SelectionModel because the selection model can also
// be bound, consider:
//
// - Both Items and Selection are bound
// - The DataContext changes
// - The binding for Items updates first
// - The new items are assigned to Selection.Source
// - The binding for Selection updates, producing a new SelectionModel
// - Both the old and new SelectionModels have the incorrect Source
private class UpdateState
{
private Optional<int> _selectedIndex;
private Optional<object?> _selectedItem;
public int UpdateCount { get; set; }
public Optional<ISelectionModel> Selection { get; set; }
public Optional<IList?> SelectedItems { get; set; }
public Optional<int> SelectedIndex
{
get => _selectedIndex;
set
{
_selectedIndex = value;
_selectedItem = default;
}
}
public Optional<object?> SelectedItem
{
get => _selectedItem;
set
{
_selectedItem = value;
_selectedIndex = default;
}
}
}
} }
} }

273
src/Avalonia.Controls/Selection/InternalSelectionModel.cs

@ -0,0 +1,273 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Collections;
#nullable enable
namespace Avalonia.Controls.Selection
{
internal class InternalSelectionModel : SelectionModel<object?>
{
private IList? _writableSelectedItems;
private bool _ignoreModelChanges;
private bool _ignoreSelectedItemsChanges;
public InternalSelectionModel()
{
SelectionChanged += OnSelectionChanged;
SourceReset += OnSourceReset;
}
[AllowNull]
public IList WritableSelectedItems
{
get
{
if (_writableSelectedItems is null)
{
_writableSelectedItems = new AvaloniaList<object?>();
SubscribeToSelectedItems();
}
return _writableSelectedItems;
}
set
{
value ??= new AvaloniaList<object?>();
if (value.IsFixedSize)
{
throw new NotSupportedException("Cannot assign fixed size selection to SelectedItems.");
}
if (_writableSelectedItems != value)
{
UnsubscribeFromSelectedItems();
_writableSelectedItems = value;
SyncFromSelectedItems();
SubscribeToSelectedItems();
if (ItemsView is null)
{
SetInitSelectedItems(value);
}
RaisePropertyChanged(nameof(WritableSelectedItems));
}
}
}
private protected override void SetSource(IEnumerable? value)
{
object?[]? oldSelection = null;
if (Source is object && value is object)
{
oldSelection = new object?[WritableSelectedItems.Count];
WritableSelectedItems.CopyTo(oldSelection, 0);
}
try
{
_ignoreSelectedItemsChanges = true;
base.SetSource(value);
}
finally
{
_ignoreSelectedItemsChanges = false;
}
if (oldSelection is null)
{
SyncToSelectedItems();
}
else
{
foreach (var i in oldSelection)
{
var index = ItemsView!.IndexOf(i);
Select(index);
}
}
}
private void SyncToSelectedItems()
{
if (_writableSelectedItems is object)
{
try
{
_ignoreSelectedItemsChanges = true;
_writableSelectedItems.Clear();
foreach (var i in base.SelectedItems)
{
_writableSelectedItems.Add(i);
}
}
finally
{
_ignoreSelectedItemsChanges = false;
}
}
}
private void SyncFromSelectedItems()
{
if (Source is null || _writableSelectedItems is null)
{
return;
}
try
{
_ignoreModelChanges = true;
using (BatchUpdate())
{
Clear();
Add(_writableSelectedItems);
}
}
finally
{
_ignoreModelChanges = false;
}
}
private void SubscribeToSelectedItems()
{
if (_writableSelectedItems is INotifyCollectionChanged incc)
{
incc.CollectionChanged += OnSelectedItemsCollectionChanged;
}
}
private void UnsubscribeFromSelectedItems()
{
if (_writableSelectedItems is INotifyCollectionChanged incc)
{
incc.CollectionChanged += OnSelectedItemsCollectionChanged;
}
}
private void OnSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
{
if (_ignoreModelChanges)
{
return;
}
try
{
var items = WritableSelectedItems;
var deselected = e.DeselectedItems.ToList();
var selected = e.SelectedItems.ToList();
_ignoreSelectedItemsChanges = true;
foreach (var i in deselected)
{
items.Remove(i);
}
foreach (var i in selected)
{
items.Add(i);
}
}
finally
{
_ignoreSelectedItemsChanges = false;
}
}
private void OnSourceReset(object sender, EventArgs e) => SyncFromSelectedItems();
private void OnSelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_ignoreSelectedItemsChanges)
{
return;
}
if (_writableSelectedItems == null)
{
throw new AvaloniaInternalException("CollectionChanged raised but we don't have items.");
}
void Remove()
{
foreach (var i in e.OldItems)
{
var index = IndexOf(Source, i);
if (index != -1)
{
Deselect(index);
}
}
}
try
{
using var operation = BatchUpdate();
_ignoreModelChanges = true;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Add(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
Remove();
break;
case NotifyCollectionChangedAction.Replace:
Remove();
Add(e.NewItems);
break;
case NotifyCollectionChangedAction.Reset:
Clear();
Add(_writableSelectedItems);
break;
}
}
finally
{
_ignoreModelChanges = false;
}
}
private void Add(IList newItems)
{
foreach (var i in newItems)
{
var index = IndexOf(Source, i);
if (index != -1)
{
Select(index);
}
}
}
private static int IndexOf(object? source, object? item)
{
if (source is IList l)
{
return l.IndexOf(item);
}
else if (source is ItemsSourceView v)
{
return v.IndexOf(item);
}
return -1;
}
}
}

106
src/Avalonia.Controls/Selection/SelectionModel.cs

@ -20,8 +20,7 @@ namespace Avalonia.Controls.Selection
private SelectedItems<T>? _selectedItems; private SelectedItems<T>? _selectedItems;
private SelectedItems<T>.Untyped? _selectedItemsUntyped; private SelectedItems<T>.Untyped? _selectedItemsUntyped;
private EventHandler<SelectionModelSelectionChangedEventArgs>? _untypedSelectionChanged; private EventHandler<SelectionModelSelectionChangedEventArgs>? _untypedSelectionChanged;
[AllowNull] private T _initSelectedItem = default; private IList? _initSelectedItems;
private bool _hasInitSelectedItem;
public SelectionModel() public SelectionModel()
{ {
@ -82,7 +81,19 @@ namespace Avalonia.Controls.Selection
[MaybeNull, AllowNull] [MaybeNull, AllowNull]
public T SelectedItem public T SelectedItem
{ {
get => ItemsView is object ? GetItemAt(_selectedIndex) : _initSelectedItem; get
{
if (ItemsView is object)
{
return GetItemAt(_selectedIndex);
}
else if (_initSelectedItems is object && _initSelectedItems.Count > 0)
{
return (T)_initSelectedItems[0];
}
return default;
}
set set
{ {
if (ItemsView is object) if (ItemsView is object)
@ -92,8 +103,9 @@ namespace Avalonia.Controls.Selection
else else
{ {
Clear(); Clear();
_initSelectedItem = value; #pragma warning disable CS8601
_hasInitSelectedItem = true; SetInitSelectedItems(new T[] { value });
#pragma warning restore CS8601
} }
} }
} }
@ -102,9 +114,10 @@ namespace Avalonia.Controls.Selection
{ {
get get
{ {
if (ItemsView is null && _hasInitSelectedItem) if (ItemsView is null && _initSelectedItems is object)
{ {
return new[] { _initSelectedItem }; return _initSelectedItems is IReadOnlyList<T> i ?
i : _initSelectedItems.Cast<T>().ToList();
} }
return _selectedItems ??= new SelectedItems<T>(this); return _selectedItems ??= new SelectedItems<T>(this);
@ -258,8 +271,7 @@ namespace Avalonia.Controls.Selection
o.SelectedIndex = -1; o.SelectedIndex = -1;
} }
_initSelectedItem = default; _initSelectedItems = null;
_hasInitSelectedItem = false;
} }
public void SelectAll() => SelectRange(0, int.MaxValue); public void SelectAll() => SelectRange(0, int.MaxValue);
@ -270,7 +282,7 @@ namespace Avalonia.Controls.Selection
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
private void SetSource(IEnumerable? value) private protected virtual void SetSource(IEnumerable? value)
{ {
if (base.Source != value) if (base.Source != value)
{ {
@ -292,11 +304,14 @@ namespace Avalonia.Controls.Selection
{ {
update.Operation.IsSourceUpdate = true; update.Operation.IsSourceUpdate = true;
if (_hasInitSelectedItem) if (_initSelectedItems is object && ItemsView is object)
{ {
SelectedItem = _initSelectedItem; foreach (T i in _initSelectedItems)
_initSelectedItem = default; {
_hasInitSelectedItem = false; Select(ItemsView.IndexOf(i));
}
_initSelectedItems = null;
} }
else else
{ {
@ -345,7 +360,9 @@ namespace Avalonia.Controls.Selection
LostSelection(this, EventArgs.Empty); LostSelection(this, EventArgs.Empty);
} }
CommitOperation(update.Operation); // Don't raise PropertyChanged events here as the OnSourceCollectionChanged event that
// let to this method being called will raise them if necessary.
CommitOperation(update.Operation, raisePropertyChanged: false);
} }
private protected override CollectionChangeState OnItemsAdded(int index, IList items) private protected override CollectionChangeState OnItemsAdded(int index, IList items)
@ -430,6 +447,11 @@ namespace Avalonia.Controls.Selection
RaisePropertyChanged(nameof(SelectedIndex)); RaisePropertyChanged(nameof(SelectedIndex));
} }
if (e.Action == NotifyCollectionChangedAction.Remove && e.OldStartingIndex <= oldSelectedIndex)
{
RaisePropertyChanged(nameof(SelectedItem));
}
if (oldAnchorIndex != _anchorIndex) if (oldAnchorIndex != _anchorIndex)
{ {
RaisePropertyChanged(nameof(AnchorIndex)); RaisePropertyChanged(nameof(AnchorIndex));
@ -459,6 +481,16 @@ namespace Avalonia.Controls.Selection
return true; return true;
} }
private protected void SetInitSelectedItems(IList items)
{
if (Source is object)
{
throw new InvalidOperationException("Cannot set init selected items when Source is set.");
}
_initSelectedItems = items;
}
protected override void OnSourceCollectionChangeFinished() protected override void OnSourceCollectionChangeFinished()
{ {
if (_operation is object) if (_operation is object)
@ -532,8 +564,7 @@ namespace Avalonia.Controls.Selection
o.SelectedIndex = o.AnchorIndex = start; o.SelectedIndex = o.AnchorIndex = start;
} }
_initSelectedItem = default; _initSelectedItems = null;
_hasInitSelectedItem = false;
} }
[return: MaybeNull] [return: MaybeNull]
@ -611,7 +642,7 @@ namespace Avalonia.Controls.Selection
} }
} }
private void CommitOperation(Operation operation) private void CommitOperation(Operation operation, bool raisePropertyChanged = true)
{ {
try try
{ {
@ -679,23 +710,34 @@ namespace Avalonia.Controls.Selection
} }
} }
if (oldSelectedIndex != _selectedIndex) if (raisePropertyChanged)
{ {
indexesChanged = true; if (oldSelectedIndex != _selectedIndex)
RaisePropertyChanged(nameof(SelectedIndex)); {
RaisePropertyChanged(nameof(SelectedItem)); indexesChanged = true;
} RaisePropertyChanged(nameof(SelectedIndex));
}
if (oldAnchorIndex != _anchorIndex) if (oldSelectedIndex != _selectedIndex || operation.IsSourceUpdate)
{ {
indexesChanged = true; RaisePropertyChanged(nameof(SelectedItem));
RaisePropertyChanged(nameof(AnchorIndex)); }
}
if (indexesChanged) if (oldAnchorIndex != _anchorIndex)
{ {
RaisePropertyChanged(nameof(SelectedIndexes)); indexesChanged = true;
RaisePropertyChanged(nameof(SelectedItems)); RaisePropertyChanged(nameof(AnchorIndex));
}
if (indexesChanged)
{
RaisePropertyChanged(nameof(SelectedIndexes));
}
if (indexesChanged || operation.IsSourceUpdate)
{
RaisePropertyChanged(nameof(SelectedItems));
}
} }
} }
finally finally

55
src/Avalonia.Controls/TabControl.cs

@ -1,3 +1,4 @@
using System.ComponentModel;
using System.Linq; using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
@ -66,7 +67,7 @@ namespace Avalonia.Controls
SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected); SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
ItemsPanelProperty.OverrideDefaultValue<TabControl>(DefaultPanel); ItemsPanelProperty.OverrideDefaultValue<TabControl>(DefaultPanel);
AffectsMeasure<TabControl>(TabStripPlacementProperty); AffectsMeasure<TabControl>(TabStripPlacementProperty);
SelectedIndexProperty.Changed.AddClassHandler<TabControl>((x, e) => x.UpdateSelectedContent(e)); SelectedItemProperty.Changed.AddClassHandler<TabControl>((x, e) => x.UpdateSelectedContent());
} }
/// <summary> /// <summary>
@ -145,55 +146,27 @@ namespace Avalonia.Controls
protected override void OnContainersMaterialized(ItemContainerEventArgs e) protected override void OnContainersMaterialized(ItemContainerEventArgs e)
{ {
base.OnContainersMaterialized(e); base.OnContainersMaterialized(e);
UpdateSelectedContent();
if (SelectedContent != null || SelectedIndex == -1)
{
return;
}
var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(SelectedIndex);
if (container == null)
{
return;
}
UpdateSelectedContent(container);
} }
private void UpdateSelectedContent(AvaloniaPropertyChangedEventArgs e) protected override void OnContainersRecycled(ItemContainerEventArgs e)
{ {
var index = (int)e.NewValue; base.OnContainersRecycled(e);
UpdateSelectedContent();
if (index == -1)
{
SelectedContentTemplate = null;
SelectedContent = null;
return;
}
var container = (TabItem)ItemContainerGenerator.ContainerFromIndex(index);
if (container == null)
{
return;
}
UpdateSelectedContent(container);
} }
private void UpdateSelectedContent(IContentControl item) private void UpdateSelectedContent()
{ {
if (SelectedContentTemplate != item.ContentTemplate) if (SelectedIndex == -1)
{ {
SelectedContentTemplate = item.ContentTemplate; SelectedContent = SelectedContentTemplate = null;
} }
else
if (SelectedContent != item.Content)
{ {
SelectedContent = item.Content; var container = SelectedItem as IContentControl ??
ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as IContentControl;
SelectedContentTemplate = container?.ContentTemplate;
SelectedContent = container?.Content;
} }
} }

283
src/Avalonia.Controls/Utils/SelectedItemsSync.cs

@ -1,283 +0,0 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Selection;
#nullable enable
namespace Avalonia.Controls.Utils
{
/// <summary>
/// Synchronizes an <see cref="ISelectionModel"/> with a list of SelectedItems.
/// </summary>
internal class SelectedItemsSync : IDisposable
{
private ISelectionModel _selectionModel;
private IList _selectedItems;
private bool _updatingItems;
private bool _updatingModel;
public SelectedItemsSync(ISelectionModel model)
{
_selectionModel = model ?? throw new ArgumentNullException(nameof(model));
_selectedItems = new AvaloniaList<object?>();
SyncSelectedItemsWithSelectionModel();
SubscribeToSelectedItems(_selectedItems);
SubscribeToSelectionModel(model);
}
public ISelectionModel SelectionModel
{
get => _selectionModel;
set
{
if (_selectionModel != value)
{
value = value ?? throw new ArgumentNullException(nameof(value));
UnsubscribeFromSelectionModel(_selectionModel);
_selectionModel = value;
SubscribeToSelectionModel(_selectionModel);
SyncSelectedItemsWithSelectionModel();
}
}
}
public IList SelectedItems
{
get => _selectedItems;
set
{
value ??= new AvaloniaList<object?>();
if (_selectedItems != value)
{
if (value.IsFixedSize)
{
throw new NotSupportedException(
"Cannot assign fixed size selection to SelectedItems.");
}
UnsubscribeFromSelectedItems(_selectedItems);
_selectedItems = value;
SubscribeToSelectedItems(_selectedItems);
SyncSelectionModelWithSelectedItems();
}
}
}
public void Dispose()
{
UnsubscribeFromSelectedItems(_selectedItems);
UnsubscribeFromSelectionModel(_selectionModel);
}
private void SyncSelectedItemsWithSelectionModel()
{
_updatingItems = true;
try
{
_selectedItems.Clear();
if (_selectionModel.Source is object)
{
foreach (var i in _selectionModel.SelectedItems)
{
_selectedItems.Add(i);
}
}
}
finally
{
_updatingItems = false;
}
}
private void SyncSelectionModelWithSelectedItems()
{
_updatingModel = true;
try
{
if (_selectionModel.Source is object)
{
using (_selectionModel.BatchUpdate())
{
SelectionModel.Clear();
Add(_selectedItems);
}
}
}
finally
{
_updatingModel = false;
}
}
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_updatingItems)
{
return;
}
if (_selectedItems == null)
{
throw new AvaloniaInternalException("CollectionChanged raised but we don't have items.");
}
void Remove()
{
foreach (var i in e.OldItems)
{
var index = IndexOf(SelectionModel.Source, i);
if (index != -1)
{
SelectionModel.Deselect(index);
}
}
}
try
{
using var operation = SelectionModel.BatchUpdate();
_updatingModel = true;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Add(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
Remove();
break;
case NotifyCollectionChangedAction.Replace:
Remove();
Add(e.NewItems);
break;
case NotifyCollectionChangedAction.Reset:
SelectionModel.Clear();
Add(_selectedItems);
break;
}
}
finally
{
_updatingModel = false;
}
}
private void Add(IList newItems)
{
foreach (var i in newItems)
{
var index = IndexOf(SelectionModel.Source, i);
if (index != -1)
{
SelectionModel.Select(index);
}
}
}
private void SelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ISelectionModel.Source))
{
if (_selectedItems.Count > 0)
{
SyncSelectionModelWithSelectedItems();
}
else
{
SyncSelectedItemsWithSelectionModel();
}
}
}
private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
{
if (_updatingModel || _selectionModel.Source is null)
{
return;
}
try
{
var deselected = e.DeselectedItems.ToList();
var selected = e.SelectedItems.ToList();
_updatingItems = true;
foreach (var i in deselected)
{
_selectedItems.Remove(i);
}
foreach (var i in selected)
{
_selectedItems.Add(i);
}
}
finally
{
_updatingItems = false;
}
}
private void SelectionModelSourceReset(object sender, EventArgs e)
{
SyncSelectionModelWithSelectedItems();
}
private void SubscribeToSelectedItems(IList selectedItems)
{
if (selectedItems is INotifyCollectionChanged incc)
{
incc.CollectionChanged += SelectedItemsCollectionChanged;
}
}
private void SubscribeToSelectionModel(ISelectionModel model)
{
model.PropertyChanged += SelectionModelPropertyChanged;
model.SelectionChanged += SelectionModelSelectionChanged;
model.SourceReset += SelectionModelSourceReset;
}
private void UnsubscribeFromSelectedItems(IList selectedItems)
{
if (selectedItems is INotifyCollectionChanged incc)
{
incc.CollectionChanged -= SelectedItemsCollectionChanged;
}
}
private void UnsubscribeFromSelectionModel(ISelectionModel model)
{
model.PropertyChanged -= SelectionModelPropertyChanged;
model.SelectionChanged -= SelectionModelSelectionChanged;
model.SourceReset -= SelectionModelSourceReset;
}
private static int IndexOf(object? source, object? item)
{
if (source is IList l)
{
return l.IndexOf(item);
}
else if (source is ItemsSourceView v)
{
return v.IndexOf(item);
}
return -1;
}
}
}

6
src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs

@ -38,11 +38,13 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport
public HtmlWebSocketTransport(IAvaloniaRemoteTransportConnection signalTransport, Uri listenUri) public HtmlWebSocketTransport(IAvaloniaRemoteTransportConnection signalTransport, Uri listenUri)
{ {
if (listenUri.Scheme != "http") if (listenUri.Scheme != "http")
throw new ArgumentException("listenUri"); throw new ArgumentException("URI scheme is not HTTP.", nameof(listenUri));
var resourcePrefix = "Avalonia.DesignerSupport.Remote.HtmlTransport.webapp.build."; var resourcePrefix = "Avalonia.DesignerSupport.Remote.HtmlTransport.webapp.build.";
_resources = typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceNames() _resources = typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceNames()
.Where(r => r.StartsWith(resourcePrefix) && r.EndsWith(".gz")).ToDictionary( .Where(r => r.StartsWith(resourcePrefix, StringComparison.OrdinalIgnoreCase)
&& r.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
.ToDictionary(
r => r.Substring(resourcePrefix.Length).Substring(0,r.Length-resourcePrefix.Length-3), r => r.Substring(resourcePrefix.Length).Substring(0,r.Length-resourcePrefix.Length-3),
r => r =>
{ {

12
src/Avalonia.Input/KeyGesture.cs

@ -1,7 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
namespace Avalonia.Input namespace Avalonia.Input
@ -29,7 +27,7 @@ namespace Avalonia.Input
KeyModifiers = modifiers; KeyModifiers = modifiers;
} }
public bool Equals(KeyGesture other) public bool Equals(KeyGesture? other)
{ {
if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(this, other)) return true;
@ -37,12 +35,12 @@ namespace Avalonia.Input
return Key == other.Key && KeyModifiers == other.KeyModifiers; return Key == other.Key && KeyModifiers == other.KeyModifiers;
} }
public override bool Equals(object obj) public override bool Equals(object? obj)
{ {
if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true; if (ReferenceEquals(this, obj)) return true;
return obj is KeyGesture && Equals((KeyGesture)obj); return obj is KeyGesture gesture && Equals(gesture);
} }
public override int GetHashCode() public override int GetHashCode()
@ -53,12 +51,12 @@ namespace Avalonia.Input
} }
} }
public static bool operator ==(KeyGesture left, KeyGesture right) public static bool operator ==(KeyGesture? left, KeyGesture? right)
{ {
return Equals(left, right); return Equals(left, right);
} }
public static bool operator !=(KeyGesture left, KeyGesture right) public static bool operator !=(KeyGesture? left, KeyGesture? right)
{ {
return !Equals(left, right); return !Equals(left, right);
} }

4
src/Avalonia.Input/PointerEventArgs.cs

@ -86,14 +86,14 @@ namespace Avalonia.Input
} }
[Obsolete("Use GetCurrentPoint")] [Obsolete("Use GetCurrentPoint")]
public PointerPoint GetPointerPoint(IVisual relativeTo) => GetCurrentPoint(relativeTo); public PointerPoint GetPointerPoint(IVisual? relativeTo) => GetCurrentPoint(relativeTo);
/// <summary> /// <summary>
/// Returns the PointerPoint associated with the current event /// Returns the PointerPoint associated with the current event
/// </summary> /// </summary>
/// <param name="relativeTo">The visual which coordinate system to use. Pass null for toplevel coordinate system</param> /// <param name="relativeTo">The visual which coordinate system to use. Pass null for toplevel coordinate system</param>
/// <returns></returns> /// <returns></returns>
public PointerPoint GetCurrentPoint(IVisual relativeTo) public PointerPoint GetCurrentPoint(IVisual? relativeTo)
=> new PointerPoint(Pointer, GetPosition(relativeTo), _properties); => new PointerPoint(Pointer, GetPosition(relativeTo), _properties);
/// <summary> /// <summary>

9
src/Avalonia.OpenGL/Angle/AngleEglInterface.cs

@ -17,14 +17,19 @@ namespace Avalonia.OpenGL.Angle
static Func<string, IntPtr> LoadAngle() static Func<string, IntPtr> LoadAngle()
{ {
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new PlatformNotSupportedException();
{ {
var disp = eglGetProcAddress("eglGetPlatformDisplayEXT"); var disp = eglGetProcAddress("eglGetPlatformDisplayEXT");
if (disp == IntPtr.Zero) if (disp == IntPtr.Zero)
{
throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point"); throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point");
}
return eglGetProcAddress; return eglGetProcAddress;
} }
throw new PlatformNotSupportedException();
} }
} }

6
src/Avalonia.OpenGL/AngleOptions.cs

@ -10,6 +10,12 @@ namespace Avalonia.OpenGL
DirectX11 DirectX11
} }
public IList<GlVersion> GlProfiles { get; set; } = new List<GlVersion>
{
new GlVersion(GlProfileType.OpenGLES, 3, 0),
new GlVersion(GlProfileType.OpenGLES, 2, 0)
};
public IList<PlatformApi> AllowedPlatformApis { get; set; } = null; public IList<PlatformApi> AllowedPlatformApis { get; set; } = null;
} }
} }

38
src/Avalonia.OpenGL/EglDisplay.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Platform.Interop; using Avalonia.Platform.Interop;
using static Avalonia.OpenGL.EglConsts; using static Avalonia.OpenGL.EglConsts;
@ -61,20 +62,43 @@ namespace Avalonia.OpenGL
if (!_egl.Initialize(_display, out var major, out var minor)) if (!_egl.Initialize(_display, out var major, out var minor))
throw OpenGlException.GetFormattedException("eglInitialize", _egl); throw OpenGlException.GetFormattedException("eglInitialize", _egl);
foreach (var cfg in new[] var glProfiles = AvaloniaLocator.Current.GetService<AngleOptions>()?.GlProfiles
?? new[]
{
new GlVersion(GlProfileType.OpenGLES, 3, 0),
new GlVersion(GlProfileType.OpenGLES, 2, 0)
};
var cfgs = glProfiles.Select(x =>
{ {
new var typeBit = EGL_OPENGL_ES3_BIT;
switch (x.Major)
{
case 2:
typeBit = EGL_OPENGL_ES2_BIT;
break;
case 1:
typeBit = EGL_OPENGL_ES_BIT;
break;
}
return new
{ {
Attributes = new[] Attributes = new[]
{ {
EGL_CONTEXT_CLIENT_VERSION, 2, EGL_CONTEXT_MAJOR_VERSION, x.Major,
EGL_CONTEXT_MINOR_VERSION, x.Minor,
EGL_NONE EGL_NONE
}, },
Api = EGL_OPENGL_ES_API, Api = EGL_OPENGL_ES_API,
RenderableTypeBit = EGL_OPENGL_ES2_BIT, RenderableTypeBit = typeBit,
Version = new GlVersion(GlProfileType.OpenGLES, 2, 0) Version = x
} };
}) });
foreach (var cfg in cfgs)
{ {
if (!_egl.BindApi(cfg.Api)) if (!_egl.BindApi(cfg.Api))
continue; continue;

2
src/Avalonia.X11/Glx/GlxDisplay.cs

@ -18,7 +18,7 @@ namespace Avalonia.X11.Glx
public XVisualInfo* VisualInfo => _visual; public XVisualInfo* VisualInfo => _visual;
public GlxContext DeferredContext { get; } public GlxContext DeferredContext { get; }
public GlxInterface Glx { get; } = new GlxInterface(); public GlxInterface Glx { get; } = new GlxInterface();
public GlxDisplay(X11Info x11, List<GlVersion> probeProfiles) public GlxDisplay(X11Info x11, IList<GlVersion> probeProfiles)
{ {
_x11 = x11; _x11 = x11;
_probeProfiles = probeProfiles.ToList(); _probeProfiles = probeProfiles.ToList();

4
src/Avalonia.X11/Glx/GlxPlatformFeature.cs

@ -12,7 +12,7 @@ namespace Avalonia.X11.Glx
public GlxContext DeferredContext { get; private set; } public GlxContext DeferredContext { get; private set; }
public IGlContext MainContext => DeferredContext; public IGlContext MainContext => DeferredContext;
public static bool TryInitialize(X11Info x11, List<GlVersion> glProfiles) public static bool TryInitialize(X11Info x11, IList<GlVersion> glProfiles)
{ {
var feature = TryCreate(x11, glProfiles); var feature = TryCreate(x11, glProfiles);
if (feature != null) if (feature != null)
@ -24,7 +24,7 @@ namespace Avalonia.X11.Glx
return false; return false;
} }
public static GlxGlPlatformFeature TryCreate(X11Info x11, List<GlVersion> glProfiles) public static GlxGlPlatformFeature TryCreate(X11Info x11, IList<GlVersion> glProfiles)
{ {
try try
{ {

4
src/Avalonia.X11/X11Platform.cs

@ -103,7 +103,7 @@ namespace Avalonia
public bool UseDBusMenu { get; set; } public bool UseDBusMenu { get; set; }
public bool UseDeferredRendering { get; set; } = true; public bool UseDeferredRendering { get; set; } = true;
public List<GlVersion> GlProfiles { get; set; } = new List<GlVersion> public IList<GlVersion> GlProfiles { get; set; } = new List<GlVersion>
{ {
new GlVersion(GlProfileType.OpenGL, 4, 0), new GlVersion(GlProfileType.OpenGL, 4, 0),
new GlVersion(GlProfileType.OpenGL, 3, 2), new GlVersion(GlProfileType.OpenGL, 3, 2),
@ -113,7 +113,7 @@ namespace Avalonia
new GlVersion(GlProfileType.OpenGLES, 2, 0) new GlVersion(GlProfileType.OpenGLES, 2, 0)
}; };
public List<string> GlxRendererBlacklist { get; set; } = new List<string> public IList<string> GlxRendererBlacklist { get; set; } = new List<string>
{ {
// llvmpipe is a software GL rasterizer. If it's returned by glGetString, // llvmpipe is a software GL rasterizer. If it's returned by glGetString,
// that usually means that something in the system is horribly misconfigured // that usually means that something in the system is horribly misconfigured

447
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -554,6 +554,44 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.False(items.Single().IsSelected); Assert.False(items.Single().IsSelected);
} }
[Fact]
public void Removing_Selected_Item_Should_Update_Selection_With_AlwaysSelected()
{
var item0 = new Item();
var item1 = new Item();
var items = new AvaloniaList<Item>
{
item0,
item1,
};
var target = new TestSelector
{
Items = items,
Template = Template(),
SelectionMode = SelectionMode.AlwaysSelected,
};
Prepare(target);
target.SelectedIndex = 1;
Assert.Equal(items[1], target.SelectedItem);
Assert.Equal(1, target.SelectedIndex);
SelectionChangedEventArgs receivedArgs = null;
target.SelectionChanged += (_, args) => receivedArgs = args;
items.RemoveAt(1);
Assert.Same(item0, target.SelectedItem);
Assert.Equal(0, target.SelectedIndex);
Assert.NotNull(receivedArgs);
Assert.Equal(new[] { item0 }, receivedArgs.AddedItems);
Assert.Equal(new[] { item1 }, receivedArgs.RemovedItems);
Assert.True(items.Single().IsSelected);
}
[Fact] [Fact]
public void Removing_Selected_Item_Should_Clear_Selection_With_BeginInit() public void Removing_Selected_Item_Should_Clear_Selection_With_BeginInit()
{ {
@ -771,6 +809,186 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.True(called); Assert.True(called);
} }
[Fact]
public void Setting_SelectedIndex_Should_Raise_PropertyChanged_Events()
{
var items = new ObservableCollection<string> { "foo", "bar", "baz" };
var target = new TestSelector
{
Items = items,
Template = Template(),
};
var selectedIndexRaised = 0;
var selectedItemRaised = 0;
target.PropertyChanged += (s, e) =>
{
if (e.Property == SelectingItemsControl.SelectedIndexProperty)
{
Assert.Equal(-1, e.OldValue);
Assert.Equal(1, e.NewValue);
++selectedIndexRaised;
}
else if (e.Property == SelectingItemsControl.SelectedItemProperty)
{
Assert.Null(e.OldValue);
Assert.Equal("bar", e.NewValue);
++selectedItemRaised;
}
};
target.SelectedIndex = 1;
Assert.Equal(1, selectedIndexRaised);
Assert.Equal(1, selectedItemRaised);
}
[Fact]
public void Removing_Selected_Item_Should_Raise_PropertyChanged_Events()
{
var items = new ObservableCollection<string> { "foo", "bar", "baz" };
var target = new TestSelector
{
Items = items,
Template = Template(),
};
var selectedIndexRaised = 0;
var selectedItemRaised = 0;
target.SelectedIndex = 1;
target.PropertyChanged += (s, e) =>
{
if (e.Property == SelectingItemsControl.SelectedIndexProperty)
{
Assert.Equal(1, e.OldValue);
Assert.Equal(-1, e.NewValue);
++selectedIndexRaised;
}
else if (e.Property == SelectingItemsControl.SelectedItemProperty)
{
Assert.Equal("bar", e.OldValue);
Assert.Null(e.NewValue);
}
};
items.RemoveAt(1);
Assert.Equal(1, selectedIndexRaised);
Assert.Equal(0, selectedItemRaised);
}
[Fact]
public void Removing_Selected_Item0_Should_Raise_PropertyChanged_Events_With_AlwaysSelected()
{
var items = new ObservableCollection<string> { "foo", "bar", "baz" };
var target = new TestSelector
{
Items = items,
Template = Template(),
SelectionMode = SelectionMode.AlwaysSelected,
};
var selectedIndexRaised = 0;
var selectedItemRaised = 0;
target.SelectedIndex = 0;
target.PropertyChanged += (s, e) =>
{
if (e.Property == SelectingItemsControl.SelectedIndexProperty)
{
++selectedIndexRaised;
}
else if (e.Property == SelectingItemsControl.SelectedItemProperty)
{
Assert.Equal("foo", e.OldValue);
Assert.Equal("bar", e.NewValue);
++selectedItemRaised;
}
};
items.RemoveAt(0);
Assert.Equal(0, selectedIndexRaised);
Assert.Equal(1, selectedItemRaised);
}
[Fact]
public void Removing_Selected_Item1_Should_Raise_PropertyChanged_Events_With_AlwaysSelected()
{
var items = new ObservableCollection<string> { "foo", "bar", "baz" };
var target = new TestSelector
{
Items = items,
Template = Template(),
SelectionMode = SelectionMode.AlwaysSelected,
};
var selectedIndexRaised = 0;
var selectedItemRaised = 0;
target.SelectedIndex = 1;
target.PropertyChanged += (s, e) =>
{
if (e.Property == SelectingItemsControl.SelectedIndexProperty)
{
Assert.Equal(1, e.OldValue);
Assert.Equal(0, e.NewValue);
++selectedIndexRaised;
}
else if (e.Property == SelectingItemsControl.SelectedItemProperty)
{
Assert.Equal("bar", e.OldValue);
Assert.Equal("foo", e.NewValue);
}
};
items.RemoveAt(1);
Assert.Equal(1, selectedIndexRaised);
Assert.Equal(0, selectedItemRaised);
}
[Fact]
public void Removing_Item_Before_Selection_Should_Raise_PropertyChanged_Events()
{
var items = new ObservableCollection<string> { "foo", "bar", "baz" };
var target = new SelectingItemsControl
{
Items = items,
Template = Template(),
};
var selectedIndexRaised = 0;
var selectedItemRaised = 0;
target.SelectedIndex = 1;
target.PropertyChanged += (s, e) =>
{
if (e.Property == SelectingItemsControl.SelectedIndexProperty)
{
Assert.Equal(1, e.OldValue);
Assert.Equal(0, e.NewValue);
++selectedIndexRaised;
}
else if (e.Property == SelectingItemsControl.SelectedItemProperty)
{
++selectedItemRaised;
}
};
items.RemoveAt(0);
Assert.Equal(1, selectedIndexRaised);
Assert.Equal(0, selectedItemRaised);
}
[Fact] [Fact]
public void Order_Of_Setting_Items_And_SelectedIndex_During_Initialization_Should_Not_Matter() public void Order_Of_Setting_Items_And_SelectedIndex_During_Initialization_Should_Not_Matter()
{ {
@ -1184,12 +1402,36 @@ namespace Avalonia.Controls.UnitTests.Primitives
Items = items, Items = items,
}; };
var raised = false;
Prepare(target); Prepare(target);
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
target.SelectedIndex = 2;
Assert.True(raised);
}
[Fact]
public void AutoScrollToSelectedItem_Causes_Scroll_To_Initial_SelectedItem()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
};
var raised = false; var raised = false;
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
target.SelectedIndex = 2; target.SelectedIndex = 2;
Prepare(target);
Assert.True(raised); Assert.True(raised);
} }
@ -1233,6 +1475,99 @@ namespace Avalonia.Controls.UnitTests.Primitives
} }
} }
[Fact]
public void AutoScrollToSelectedItem_Scrolls_When_Reattached_To_Visual_Tree_If_Selection_Changed_While_Detached_From_Visual_Tree()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
SelectedIndex = 2,
};
var raised = false;
Prepare(target);
var root = (TestRoot)target.Parent;
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
root.Child = null;
target.SelectedIndex = 1;
root.Child = target;
Assert.True(raised);
}
[Fact]
public void AutoScrollToSelectedItem_Doesnt_Scroll_If_Reattached_To_Visual_Tree_With_No_Selection_Change()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
SelectedIndex = 2,
};
var raised = false;
Prepare(target);
var root = (TestRoot)target.Parent;
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
root.Child = null;
root.Child = target;
Assert.False(raised);
}
[Fact]
public void AutoScrollToSelectedItem_Causes_Scroll_When_Turned_On()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
AutoScrollToSelectedItem = false,
};
Prepare(target);
var raised = false;
target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
target.SelectedIndex = 2;
Assert.False(raised);
target.AutoScrollToSelectedItem = true;
Assert.True(raised);
}
[Fact] [Fact]
public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization() public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization()
{ {
@ -1376,6 +1711,108 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(new[] { "foo" }, target.SelectedItems); Assert.Equal(new[] { "foo" }, target.SelectedItems);
} }
[Fact]
public void Preserves_Initial_SelectedItems_When_Bound()
{
// Issue #4272 (there are two issues there, this addresses the second one).
var vm = new SelectionViewModel
{
Items = { "foo", "bar", "baz" },
SelectedItems = { "bar" },
};
var target = new ListBox
{
[!ListBox.ItemsProperty] = new Binding("Items"),
[!ListBox.SelectedItemsProperty] = new Binding("SelectedItems"),
DataContext = vm,
};
Prepare(target);
Assert.Equal(1, target.SelectedIndex);
Assert.Equal(new[] { 1 }, target.Selection.SelectedIndexes);
Assert.Equal("bar", target.SelectedItem);
Assert.Equal(new[] { "bar" }, target.SelectedItems);
}
[Fact]
public void Preserves_SelectedItem_When_Items_Changed()
{
// Issue #4048
var target = new SelectingItemsControl
{
Items = new[] { "foo", "bar", "baz"},
SelectedItem = "bar",
};
Prepare(target);
Assert.Equal(1, target.SelectedIndex);
Assert.Equal("bar", target.SelectedItem);
target.Items = new[] { "qux", "foo", "bar" };
Assert.Equal(2, target.SelectedIndex);
Assert.Equal("bar", target.SelectedItem);
}
[Fact]
public void Setting_SelectedItems_Raises_PropertyChanged()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar", "baz" },
};
var raised = 0;
var newValue = new AvaloniaList<object>();
Prepare(target);
target.PropertyChanged += (s, e) =>
{
if (e.Property == ListBox.SelectedItemsProperty)
{
Assert.Null(e.OldValue);
Assert.Same(newValue, e.NewValue);
++raised;
}
};
target.SelectedItems = newValue;
Assert.Equal(1, raised);
}
[Fact]
public void Setting_Selection_Raises_SelectedItems_PropertyChanged()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar", "baz" },
};
var raised = 0;
var oldValue = target.SelectedItems;
Prepare(target);
target.PropertyChanged += (s, e) =>
{
if (e.Property == ListBox.SelectedItemsProperty)
{
Assert.Same(oldValue, e.OldValue);
Assert.Null(e.NewValue);
++raised;
}
};
target.Selection = new SelectionModel<int>();
Assert.Equal(1, raised);
}
private static void Prepare(SelectingItemsControl target) private static void Prepare(SelectingItemsControl target)
{ {
var root = new TestRoot var root = new TestRoot
@ -1445,6 +1882,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
public SelectionViewModel() public SelectionViewModel()
{ {
Items = new ObservableCollection<string>(); Items = new ObservableCollection<string>();
SelectedItems = new ObservableCollection<string>();
} }
public int SelectedIndex public int SelectedIndex
@ -1458,6 +1896,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
} }
public ObservableCollection<string> Items { get; } public ObservableCollection<string> Items { get; }
public ObservableCollection<string> SelectedItems { get; }
} }
private class RootWithItems : TestRoot private class RootWithItems : TestRoot
@ -1484,6 +1923,12 @@ namespace Avalonia.Controls.UnitTests.Primitives
set => base.Selection = value; set => base.Selection = value;
} }
public new IList SelectedItems
{
get => base.SelectedItems;
set => base.SelectedItems = value;
}
public new SelectionMode SelectionMode public new SelectionMode SelectionMode
{ {
get => base.SelectionMode; get => base.SelectionMode;

254
tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs

@ -0,0 +1,254 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Controls.Selection;
using Xunit;
namespace Avalonia.Controls.UnitTests.Selection
{
public class InternalSelectionModelTests
{
[Fact]
public void Selecting_Item_Adds_To_WritableSelectedItems()
{
var target = CreateTarget();
target.Select(0);
Assert.Equal(new[] { "foo" }, target.WritableSelectedItems);
}
[Fact]
public void Selecting_Duplicate_On_Model_Adds_To_WritableSelectedItems()
{
var target = CreateTarget(source: new[] { "foo", "bar", "baz", "foo", "bar", "baz" });
target.SelectRange(1, 4);
Assert.Equal(new[] { "bar", "baz", "foo", "bar" }, target.WritableSelectedItems);
}
[Fact]
public void Deselecting_On_Model_Removes_SelectedItem()
{
var target = CreateTarget();
target.SelectRange(1, 2);
target.Deselect(1);
Assert.Equal(new[] { "baz" }, target.WritableSelectedItems);
}
[Fact]
public void Deselecting_Duplicate_On_Model_Removes_SelectedItem()
{
var target = CreateTarget(source: new[] { "foo", "bar", "baz", "foo", "bar", "baz" });
target.SelectRange(1, 2);
target.Select(4);
target.Deselect(4);
Assert.Equal(new[] { "baz", "bar" }, target.WritableSelectedItems);
}
[Fact]
public void Adding_To_WritableSelectedItems_Selects_On_Model()
{
var target = CreateTarget();
target.SelectRange(1, 2);
target.WritableSelectedItems.Add("foo");
Assert.Equal(new[] { 0, 1, 2 }, target.SelectedIndexes);
Assert.Equal(new[] { "bar", "baz", "foo" }, target.WritableSelectedItems);
}
[Fact]
public void Removing_From_WritableSelectedItems_Deselects_On_Model()
{
var target = CreateTarget();
target.SelectRange(1, 2);
target.WritableSelectedItems.Remove("baz");
Assert.Equal(new[] { 1 }, target.SelectedIndexes);
Assert.Equal(new[] { "bar" }, target.WritableSelectedItems);
}
[Fact]
public void Replacing_SelectedItem_Updates_Model()
{
var target = CreateTarget();
target.SelectRange(1, 2);
target.WritableSelectedItems[0] = "foo";
Assert.Equal(new[] { 0, 2 }, target.SelectedIndexes);
Assert.Equal(new[] { "foo", "baz" }, target.WritableSelectedItems);
}
[Fact]
public void Clearing_WritableSelectedItems_Updates_Model()
{
var target = CreateTarget();
target.WritableSelectedItems.Clear();
Assert.Empty(target.SelectedIndexes);
}
[Fact]
public void Setting_WritableSelectedItems_Updates_Model()
{
var target = CreateTarget();
var oldItems = target.WritableSelectedItems;
var newItems = new AvaloniaList<string> { "foo", "baz" };
target.WritableSelectedItems = newItems;
Assert.Equal(new[] { 0, 2 }, target.SelectedIndexes);
Assert.Same(newItems, target.WritableSelectedItems);
Assert.NotSame(oldItems, target.WritableSelectedItems);
Assert.Equal(new[] { "foo", "baz" }, newItems);
}
[Fact]
public void Setting_Items_To_Null_Clears_Selection()
{
var target = CreateTarget();
target.SelectRange(1, 2);
target.WritableSelectedItems = null;
Assert.Empty(target.SelectedIndexes);
}
[Fact]
public void Setting_Items_To_Null_Creates_Empty_Items()
{
var target = CreateTarget();
var oldItems = target.WritableSelectedItems;
target.WritableSelectedItems = null;
Assert.NotNull(target.WritableSelectedItems);
Assert.NotSame(oldItems, target.WritableSelectedItems);
Assert.IsType<AvaloniaList<object>>(target.WritableSelectedItems);
}
[Fact]
public void Adds_Null_WritableSelectedItems_When_Source_Is_Null()
{
var target = CreateTarget(nullSource: true);
target.SelectRange(1, 2);
Assert.Equal(new object[] { null, null }, target.WritableSelectedItems);
}
[Fact]
public void Updates_WritableSelectedItems_When_Source_Changes_From_Null()
{
var target = CreateTarget(nullSource: true);
target.SelectRange(1, 2);
Assert.Equal(new object[] { null, null }, target.WritableSelectedItems);
target.Source = new[] { "foo", "bar", "baz" };
Assert.Equal(new[] { "bar", "baz" }, target.WritableSelectedItems);
}
[Fact]
public void Updates_WritableSelectedItems_When_Source_Changes_To_Null()
{
var target = CreateTarget();
target.SelectRange(1, 2);
Assert.Equal(new[] { "bar", "baz" }, target.WritableSelectedItems);
target.Source = null;
Assert.Equal(new object[] { null, null }, target.WritableSelectedItems);
}
[Fact]
public void WritableSelectedItems_Can_Be_Set_Before_Source()
{
var target = CreateTarget(nullSource: true);
var items = new AvaloniaList<string> { "foo", "bar", "baz" };
var WritableSelectedItems = new AvaloniaList<string> { "bar" };
target.WritableSelectedItems = WritableSelectedItems;
target.Source = items;
Assert.Equal(1, target.SelectedIndex);
}
[Fact]
public void Does_Not_Accept_Fixed_Size_Items()
{
var target = CreateTarget();
Assert.Throws<NotSupportedException>(() =>
target.WritableSelectedItems = new[] { "foo", "bar", "baz" });
}
[Fact]
public void Restores_Selection_On_Items_Reset()
{
var items = new ResettingCollection(new[] { "foo", "bar", "baz" });
var target = CreateTarget(source: items);
target.SelectedIndex = 1;
items.Reset(new[] { "baz", "foo", "bar" });
Assert.Equal(2, target.SelectedIndex);
}
[Fact]
public void Preserves_Selection_On_Source_Changed()
{
var target = CreateTarget();
target.SelectedIndex = 1;
target.Source = new[] { "baz", "foo", "bar" };
Assert.Equal(2, target.SelectedIndex);
}
private static InternalSelectionModel CreateTarget(
bool singleSelect = false,
IList source = null,
bool nullSource = false)
{
source ??= !nullSource ? new[] { "foo", "bar", "baz" } : null;
var result = new InternalSelectionModel
{
SingleSelect = singleSelect,
};
((ISelectionModel)result).Source = source;
return result;
}
private class ResettingCollection : List<string>, INotifyCollectionChanged
{
public ResettingCollection(IEnumerable<string> items)
{
AddRange(items);
}
public void Reset(IEnumerable<string> items)
{
Clear();
AddRange(items);
CollectionChanged?.Invoke(
this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
}
}
}

56
tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs

@ -121,6 +121,34 @@ namespace Avalonia.Controls.UnitTests.Selection
Assert.Equal(0, raised); Assert.Equal(0, raised);
} }
[Fact]
public void Initializing_Source_Raises_SelectedItems_PropertyChanged()
{
var target = CreateTarget(false);
var selectedItemRaised = 0;
var selectedItemsRaised = 0;
target.Select(1);
target.Select(2);
target.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(target.SelectedItem))
{
++selectedItemRaised;
}
else if (e.PropertyName == nameof(target.SelectedItems))
{
++selectedItemsRaised;
}
};
target.Source = new[] { "foo", "bar", "baz" };
Assert.Equal(1, selectedItemRaised);
Assert.Equal(1, selectedItemsRaised);
}
[Fact] [Fact]
public void Initializing_Source_Respects_Range_SourceItem_Order() public void Initializing_Source_Respects_Range_SourceItem_Order()
{ {
@ -152,6 +180,34 @@ namespace Avalonia.Controls.UnitTests.Selection
Assert.Equal("bar", target.SelectedItem); Assert.Equal("bar", target.SelectedItem);
Assert.Equal(new[] { "bar" }, target.SelectedItems); Assert.Equal(new[] { "bar" }, target.SelectedItems);
} }
[Fact]
public void Changing_Source_To_Null_Raises_SelectedItems_PropertyChanged()
{
var target = CreateTarget();
var selectedItemRaised = 0;
var selectedItemsRaised = 0;
target.Select(1);
target.Select(2);
target.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(target.SelectedItem))
{
++selectedItemRaised;
}
else if (e.PropertyName == nameof(target.SelectedItems))
{
++selectedItemsRaised;
}
};
target.Source = null;
Assert.Equal(1, selectedItemRaised);
Assert.Equal(1, selectedItemsRaised);
}
} }
public class SelectedIndex public class SelectedIndex

56
tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs

@ -174,6 +174,33 @@ namespace Avalonia.Controls.UnitTests.Selection
Assert.Equal(new[] { "bar" }, target.SelectedItems); Assert.Equal(new[] { "bar" }, target.SelectedItems);
} }
[Fact]
public void Initializing_Source_Raises_SelectedItems_PropertyChanged()
{
var target = CreateTarget(false);
var selectedItemRaised = 0;
var selectedItemsRaised = 0;
target.Select(1);
target.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(target.SelectedItem))
{
++selectedItemRaised;
}
else if (e.PropertyName == nameof(target.SelectedItems))
{
++selectedItemsRaised;
}
};
target.Source = new[] { "foo", "bar", "baz" };
Assert.Equal(1, selectedItemRaised);
Assert.Equal(1, selectedItemsRaised);
}
[Fact] [Fact]
public void Changing_Source_To_Null_Doesnt_Clear_Selection() public void Changing_Source_To_Null_Doesnt_Clear_Selection()
{ {
@ -194,7 +221,7 @@ namespace Avalonia.Controls.UnitTests.Selection
} }
[Fact] [Fact]
public void Changing_Source_To_NonNUll_First_Clears_Old_Selection() public void Changing_Source_To_NonNull_First_Clears_Old_Selection()
{ {
var target = CreateTarget(); var target = CreateTarget();
var raised = 0; var raised = 0;
@ -219,6 +246,33 @@ namespace Avalonia.Controls.UnitTests.Selection
Assert.Equal(1, raised); Assert.Equal(1, raised);
} }
[Fact]
public void Changing_Source_To_Null_Raises_SelectedItems_PropertyChanged()
{
var target = CreateTarget();
var selectedItemRaised = 0;
var selectedItemsRaised = 0;
target.Select(1);
target.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(target.SelectedItem))
{
++selectedItemRaised;
}
else if (e.PropertyName == nameof(target.SelectedItems))
{
++selectedItemsRaised;
}
};
target.Source = null;
Assert.Equal(1, selectedItemRaised);
Assert.Equal(1, selectedItemsRaised);
}
[Fact] [Fact]
public void Raises_PropertyChanged() public void Raises_PropertyChanged()
{ {

84
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -122,11 +122,86 @@ namespace Avalonia.Controls.UnitTests
Items = collection, Items = collection,
}; };
target.ApplyTemplate(); Prepare(target);
target.SelectedItem = collection[1]; target.SelectedItem = collection[1];
Assert.Same(collection[1], target.SelectedItem);
Assert.Equal(collection[1].Content, target.SelectedContent);
collection.RemoveAt(1); collection.RemoveAt(1);
Assert.Same(collection[0], target.SelectedItem); Assert.Same(collection[0], target.SelectedItem);
Assert.Equal(collection[0].Content, target.SelectedContent);
}
[Fact]
public void Removal_Should_Set_New_Item0_When_Item0_Selected()
{
var collection = new ObservableCollection<TabItem>()
{
new TabItem
{
Name = "first",
Content = "foo",
},
new TabItem
{
Name = "second",
Content = "bar",
},
new TabItem
{
Name = "3rd",
Content = "barf",
},
};
var target = new TabControl
{
Template = TabControlTemplate(),
Items = collection,
};
Prepare(target);
target.SelectedItem = collection[0];
Assert.Same(collection[0], target.SelectedItem);
Assert.Equal(collection[0].Content, target.SelectedContent);
collection.RemoveAt(0);
Assert.Same(collection[0], target.SelectedItem);
Assert.Equal(collection[0].Content, target.SelectedContent);
}
[Fact]
public void Removal_Should_Set_New_Item0_When_Item0_Selected_With_DataTemplate()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var collection = new ObservableCollection<Item>()
{
new Item("first"),
new Item("second"),
new Item("3rd"),
};
var target = new TabControl
{
Template = TabControlTemplate(),
Items = collection,
};
Prepare(target);
target.SelectedItem = collection[0];
Assert.Same(collection[0], target.SelectedItem);
Assert.Equal(collection[0], target.SelectedContent);
collection.RemoveAt(0);
Assert.Same(collection[0], target.SelectedItem);
Assert.Equal(collection[0], target.SelectedContent);
} }
[Fact] [Fact]
@ -383,6 +458,13 @@ namespace Avalonia.Controls.UnitTests
}.RegisterInNameScope(scope)); }.RegisterInNameScope(scope));
} }
private void Prepare(TabControl target)
{
ApplyTemplate(target);
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
}
private void ApplyTemplate(TabControl target) private void ApplyTemplate(TabControl target)
{ {
target.ApplyTemplate(); target.ApplyTemplate();

278
tests/Avalonia.Controls.UnitTests/Utils/SelectedItemsSyncTests.cs

@ -1,278 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Controls.Selection;
using Avalonia.Controls.Utils;
using Xunit;
namespace Avalonia.Controls.UnitTests.Utils
{
public class SelectedItemsSyncTests
{
[Fact]
public void Initial_Items_Are_From_Model()
{
var target = CreateTarget();
var items = target.SelectedItems;
Assert.Equal(new[] { "bar", "baz" }, items);
}
[Fact]
public void Selecting_On_Model_Adds_Item()
{
var target = CreateTarget();
var items = target.SelectedItems;
target.SelectionModel.Select(0);
Assert.Equal(new[] { "bar", "baz", "foo" }, items);
}
[Fact]
public void Selecting_Duplicate_On_Model_Adds_Item()
{
var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" });
var items = target.SelectedItems;
target.SelectionModel.Select(4);
Assert.Equal(new[] { "bar", "baz", "bar" }, items);
}
[Fact]
public void Deselecting_On_Model_Removes_Item()
{
var target = CreateTarget();
var items = target.SelectedItems;
target.SelectionModel.Deselect(1);
Assert.Equal(new[] { "baz" }, items);
}
[Fact]
public void Deselecting_Duplicate_On_Model_Removes_Item()
{
var target = CreateTarget(new[] { "foo", "bar", "baz", "foo", "bar", "baz" });
var items = target.SelectedItems;
target.SelectionModel.Select(4);
target.SelectionModel.Deselect(4);
Assert.Equal(new[] { "baz", "bar" }, items);
}
[Fact]
public void Reassigning_Model_Resets_Items()
{
var target = CreateTarget();
var items = target.SelectedItems;
var newModel = new SelectionModel<string>
{
Source = (string[])target.SelectionModel.Source,
SingleSelect = false
};
newModel.Select(0);
newModel.Select(1);
target.SelectionModel = newModel;
Assert.Equal(new[] { "foo", "bar" }, items);
}
[Fact]
public void Reassigning_Model_Tracks_New_Model()
{
var target = CreateTarget();
var items = target.SelectedItems;
var newModel = new SelectionModel<string>
{
Source = (string[])target.SelectionModel.Source,
SingleSelect = false
};
target.SelectionModel = newModel;
newModel.Select(0);
newModel.Select(1);
Assert.Equal(new[] { "foo", "bar" }, items);
}
[Fact]
public void Adding_To_Items_Selects_On_Model()
{
var target = CreateTarget();
var items = target.SelectedItems;
items.Add("foo");
Assert.Equal(new[] { 0, 1, 2 }, target.SelectionModel.SelectedIndexes);
Assert.Equal(new[] { "bar", "baz", "foo" }, items);
}
[Fact]
public void Removing_From_Items_Deselects_On_Model()
{
var target = CreateTarget();
var items = target.SelectedItems;
items.Remove("baz");
Assert.Equal(new[] { 1 }, target.SelectionModel.SelectedIndexes);
Assert.Equal(new[] { "bar" }, items);
}
[Fact]
public void Replacing_Item_Updates_Model()
{
var target = CreateTarget();
var items = target.SelectedItems;
items[0] = "foo";
Assert.Equal(new[] { 0, 2 }, target.SelectionModel.SelectedIndexes);
Assert.Equal(new[] { "foo", "baz" }, items);
}
[Fact]
public void Clearing_Items_Updates_Model()
{
var target = CreateTarget();
var items = target.SelectedItems;
items.Clear();
Assert.Empty(target.SelectionModel.SelectedIndexes);
}
[Fact]
public void Setting_Items_Updates_Model()
{
var target = CreateTarget();
var oldItems = target.SelectedItems;
var newItems = new AvaloniaList<string> { "foo", "baz" };
target.SelectedItems = newItems;
Assert.Equal(new[] { 0, 2 }, target.SelectionModel.SelectedIndexes);
Assert.Same(newItems, target.SelectedItems);
Assert.NotSame(oldItems, target.SelectedItems);
Assert.Equal(new[] { "foo", "baz" }, newItems);
}
[Fact]
public void Setting_Items_Subscribes_To_Model()
{
var target = CreateTarget();
var items = new AvaloniaList<string> { "foo", "baz" };
target.SelectedItems = items;
target.SelectionModel.Select(1);
Assert.Equal(new[] { "foo", "baz", "bar" }, items);
}
[Fact]
public void Setting_Items_To_Null_Creates_Empty_Items()
{
var target = CreateTarget();
var oldItems = target.SelectedItems;
target.SelectedItems = null;
var newItems = Assert.IsType<AvaloniaList<object>>(target.SelectedItems);
Assert.NotSame(oldItems, newItems);
}
[Fact]
public void Handles_Null_Model_Source()
{
var model = new SelectionModel<string> { SingleSelect = false };
model.Select(1);
var target = new SelectedItemsSync(model);
var items = target.SelectedItems;
Assert.Empty(items);
model.Select(2);
model.Source = new[] { "foo", "bar", "baz" };
Assert.Equal(new[] { "bar", "baz" }, items);
}
[Fact]
public void Does_Not_Accept_Fixed_Size_Items()
{
var target = CreateTarget();
Assert.Throws<NotSupportedException>(() =>
target.SelectedItems = new[] { "foo", "bar", "baz" });
}
[Fact]
public void Selected_Items_Can_Be_Set_Before_SelectionModel_Source()
{
var model = new SelectionModel<string>();
var target = new SelectedItemsSync(model);
var items = new AvaloniaList<string> { "foo", "bar", "baz" };
var selectedItems = new AvaloniaList<string> { "bar" };
target.SelectedItems = selectedItems;
model.Source = items;
Assert.Equal(1, model.SelectedIndex);
}
[Fact]
public void Restores_Selection_On_Items_Reset()
{
var items = new ResettingCollection(new[] { "foo", "bar", "baz" });
var model = new SelectionModel<string> { Source = items };
var target = new SelectedItemsSync(model);
model.SelectedIndex = 1;
items.Reset(new[] { "baz", "foo", "bar" });
Assert.Equal(2, model.SelectedIndex);
}
private static SelectedItemsSync CreateTarget(
IEnumerable<string> items = null)
{
items ??= new[] { "foo", "bar", "baz" };
var model = new SelectionModel<string> { Source = items, SingleSelect = false };
model.SelectRange(1, 2);
var target = new SelectedItemsSync(model);
return target;
}
private class ResettingCollection : List<string>, INotifyCollectionChanged
{
public ResettingCollection(IEnumerable<string> items)
{
AddRange(items);
}
public void Reset(IEnumerable<string> items)
{
Clear();
AddRange(items);
CollectionChanged?.Invoke(
this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
}
}
}
Loading…
Cancel
Save