Browse Source

Merge branch 'master' into master

pull/4589/head
GMIKE 6 years ago
committed by GitHub
parent
commit
bba3dce4a5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      build/HarfBuzzSharp.props
  2. 60
      native/Avalonia.Native/src/OSX/window.mm
  3. 12
      samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
  4. 10
      samples/ControlCatalog/Pages/SplitViewPage.xaml
  5. 23
      src/Avalonia.Controls/Selection/SelectionModel.cs
  6. 34
      src/Avalonia.Controls/Selection/SelectionNodeBase.cs
  7. 62
      src/Avalonia.Controls/SplitView.cs
  8. 29
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  9. 87
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  10. 45
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  11. 2
      src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs
  12. 63
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  13. 32
      tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs
  14. 31
      tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs

4
build/HarfBuzzSharp.props

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

60
native/Avalonia.Native/src/OSX/window.mm

@ -1209,6 +1209,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
bool _queuedDisplayFromThread;
NSTrackingArea* _area;
bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver;
AvnInputModifiers _modifierState;
NSEvent* _lastMouseDownEvent;
bool _lastKeyHandled;
AvnPixelSize _lastPixelSize;
@ -1251,6 +1252,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_lastPixelSize.Height = 100;
_lastPixelSize.Width = 100;
[self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]];
_modifierState = AvnInputModifiersNone;
return self;
}
@ -1594,6 +1597,63 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return result;
}
- (void)flagsChanged:(NSEvent *)event
{
auto newModifierState = [self getModifiers:[event modifierFlags]];
bool isAltCurrentlyPressed = (_modifierState & Alt) == Alt;
bool isControlCurrentlyPressed = (_modifierState & Control) == Control;
bool isShiftCurrentlyPressed = (_modifierState & Shift) == Shift;
bool isCommandCurrentlyPressed = (_modifierState & Windows) == Windows;
bool isAltPressed = (newModifierState & Alt) == Alt;
bool isControlPressed = (newModifierState & Control) == Control;
bool isShiftPressed = (newModifierState & Shift) == Shift;
bool isCommandPressed = (newModifierState & Windows) == Windows;
if (isAltPressed && !isAltCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if (isAltCurrentlyPressed && !isAltPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
if (isControlPressed && !isControlCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if (isControlCurrentlyPressed && !isControlPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
if (isShiftPressed && !isShiftCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if(isShiftCurrentlyPressed && !isShiftPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
if(isCommandPressed && !isCommandCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if(isCommandCurrentlyPressed && ! isCommandPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
_modifierState = newModifierState;
[[self inputContext] handleEvent:event];
[super flagsChanged:event];
}
- (void)keyDown:(NSEvent *)event
{
[self keyboardEvent:event withType:KeyDown];

12
samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj

@ -9,6 +9,10 @@
<RootNamespace>ControlCatalog.iOS</RootNamespace>
<IPhoneResourcePrefix>Resources</IPhoneResourcePrefix>
<AssemblyName>ControlCatalogiOS</AssemblyName>
<MtouchEnableSGenConc>true</MtouchEnableSGenConc>
<MtouchHttpClientHandler>NSUrlSessionHandler</MtouchHttpClientHandler>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
<ProvisioningType>automatic</ProvisioningType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhoneSimulator' ">
<DebugSymbols>true</DebugSymbols>
@ -19,8 +23,8 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<ConsolePause>false</ConsolePause>
<MtouchArch>i386</MtouchArch>
<MtouchLink>SdkOnly</MtouchLink>
<MtouchArch>x86_64</MtouchArch>
<MtouchLink>None</MtouchLink>
<MtouchDebug>True</MtouchDebug>
<MtouchSdkVersion>9.1</MtouchSdkVersion>
<MtouchProfiling>False</MtouchProfiling>
@ -43,7 +47,7 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<MtouchLink>None</MtouchLink>
<MtouchArch>i386</MtouchArch>
<MtouchArch>x86_64</MtouchArch>
<ConsolePause>false</ConsolePause>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhone' ">
@ -173,8 +177,10 @@
<Project>{d0a739b9-3c68-4ba6-a328-41606954b6bd}</Project>
<Name>ControlCatalog</Name>
</ProjectReference>
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.80.2-preview.33" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
<Import Project="..\..\build\LegacyProject.targets" />
<Import Project="..\..\build\SkiaSharp.props" />
<Import Project="..\..\build\HarfBuzzSharp.props" />
</Project>

10
samples/ControlCatalog/Pages/SplitViewPage.xaml

@ -58,12 +58,18 @@
<SplitView.Pane>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="PANE CONTENT" FontWeight="Bold" Name="PaneHeader" Margin="5,12,0,0" />
<ListBoxItem Grid.Row="1" VerticalAlignment="Top" Margin="0 10">
<ComboBox Width="150" Grid.Row="1">
<ComboBoxItem Content="Item1"/>
<ComboBoxItem Content="Item2"/>
<ComboBoxItem Content="Item3"/>
</ComboBox>
<ListBoxItem Grid.Row="2" VerticalAlignment="Top" Margin="0 10">
<StackPanel Orientation="Horizontal">
<!--Path glyph from materialdesignicons.com-->
<Border Width="48">
@ -76,7 +82,7 @@
<TextBlock Text="People" VerticalAlignment="Center" />
</StackPanel>
</ListBoxItem>
<TextBlock Grid.Row="2" Text="Item at bottom" Margin="60,12" />
<TextBlock Grid.Row="3" Text="Item at bottom" Margin="60,12" />
</Grid>
</SplitView.Pane>

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

@ -436,6 +436,29 @@ namespace Avalonia.Controls.Selection
}
}
private protected override bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e)
{
if (!base.IsValidCollectionChange(e))
{
return false;
}
if (ItemsView is object && e.Action == NotifyCollectionChangedAction.Add)
{
if (e.NewStartingIndex <= _selectedIndex)
{
return _selectedIndex + e.NewItems.Count < ItemsView.Count;
}
if (e.NewStartingIndex <= _anchorIndex)
{
return _anchorIndex + e.NewItems.Count < ItemsView.Count;
}
}
return true;
}
protected override void OnSourceCollectionChangeFinished()
{
if (_operation is object)

34
src/Avalonia.Controls/Selection/SelectionNodeBase.cs

@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Utils;
#nullable enable
@ -234,6 +235,11 @@ namespace Avalonia.Controls.Selection
var shiftIndex = -1;
List<T>? removed = null;
if (!IsValidCollectionChange(e))
{
return;
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
@ -276,6 +282,34 @@ namespace Avalonia.Controls.Selection
}
}
private protected virtual bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e)
{
// If the selection is modified in a CollectionChanged handler before the selection
// model's CollectionChanged handler has had chance to run then we can end up with
// a selected index that refers to the *new* state of the Source intermixed with
// indexes that reference an old state of the source.
//
// There's not much we can do in this situation, so detect whether shifting the
// current selected indexes would result in an invalid index in the source, and if
// so bail.
//
// See unit test Handles_Selection_Made_In_CollectionChanged for more details.
if (ItemsView is object &&
RangesEnabled &&
Ranges.Count > 0 &&
e.Action == NotifyCollectionChangedAction.Add)
{
var lastIndex = Ranges.Last().End;
if (e.NewStartingIndex <= lastIndex)
{
return lastIndex + e.NewItems.Count < ItemsView.Count;
}
}
return true;
}
private protected struct CollectionChangeState
{
public int ShiftIndex;

62
src/Avalonia.Controls/SplitView.cs

@ -145,7 +145,7 @@ namespace Avalonia.Controls
private bool _isPaneOpen;
private Panel _pane;
private CompositeDisposable _pointerDisposables;
private IDisposable _pointerDisposable;
public SplitView()
{
@ -320,37 +320,14 @@ namespace Avalonia.Controls
var topLevel = this.VisualRoot;
if (topLevel is Window window)
{
//Logic adapted from Popup
//Basically if we're using an overlay DisplayMode, close the pane if we don't click on the pane
IDisposable subscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler,
Action<T, TEventHandler> subscribe, Action<T, TEventHandler> unsubscribe)
{
subscribe(target, handler);
return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler));
}
_pointerDisposables = new CompositeDisposable(
window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel),
InputManager.Instance?.Process.Subscribe(OnNonClientClick),
subscribeToEventHandler<Window, EventHandler>(window, Window_Deactivated,
(x, handler) => x.Deactivated += handler, (x, handler) => x.Deactivated -= handler),
subscribeToEventHandler<IWindowImpl, Action>(window.PlatformImpl, OnWindowLostFocus,
(x, handler) => x.LostFocus += handler, (x, handler) => x.LostFocus -= handler));
_pointerDisposable = window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_pointerDisposables?.Dispose();
}
private void OnWindowLostFocus()
{
if (IsPaneOpen && ShouldClosePane())
{
IsPaneOpen = false;
}
_pointerDisposable?.Dispose();
}
private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
@ -371,7 +348,12 @@ namespace Avalonia.Controls
var src = e.Source as IVisual;
while (src != null)
{
if (src == _pane)
// Make assumption that if Popup is in visual tree,
// owning control is within pane
// This works because if pane is triggered to close
// when clicked anywhere else in Window, the pane
// would close before the popup is opened
if (src == _pane || src is PopupRoot)
{
closePane = false;
break;
@ -385,31 +367,7 @@ namespace Avalonia.Controls
e.Handled = true;
}
}
private void OnNonClientClick(RawInputEventArgs obj)
{
if (!IsPaneOpen)
{
return;
}
var mouse = obj as RawPointerEventArgs;
if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
if (ShouldClosePane())
IsPaneOpen = false;
}
}
private void Window_Deactivated(object sender, EventArgs e)
{
if (IsPaneOpen && ShouldClosePane())
{
IsPaneOpen = false;
}
}
private bool ShouldClosePane()
{
return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay);

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

@ -12,12 +12,13 @@ namespace Avalonia.Diagnostics.ViewModels
private readonly IVisual _control;
private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
private AvaloniaPropertyViewModel _selectedProperty;
private string _propertyFilter;
public ControlDetailsViewModel(IVisual control, string propertyFilter)
public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
{
_control = control;
TreePage = treePage;
var properties = GetAvaloniaProperties(control)
.Concat(GetClrProperties(control))
.OrderBy(x => x, PropertyComparer.Instance)
@ -25,7 +26,6 @@ namespace Avalonia.Diagnostics.ViewModels
.ToList();
_propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList());
_propertyFilter = propertyFilter;
var view = new DataGridCollectionView(properties);
view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group)));
@ -43,19 +43,9 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public DataGridCollectionView PropertiesView { get; }
public TreePageViewModel TreePage { get; }
public string PropertyFilter
{
get => _propertyFilter;
set
{
if (RaiseAndSetIfChanged(ref _propertyFilter, value))
{
PropertiesView.Refresh();
}
}
}
public DataGridCollectionView PropertiesView { get; }
public AvaloniaPropertyViewModel SelectedProperty
{
@ -137,9 +127,14 @@ namespace Avalonia.Diagnostics.ViewModels
private bool FilterProperty(object arg)
{
if (!string.IsNullOrWhiteSpace(PropertyFilter) && arg is PropertyViewModel property)
if (!string.IsNullOrWhiteSpace(TreePage.PropertyFilter) && arg is PropertyViewModel property)
{
return property.Name.IndexOf(PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1;
if (TreePage.UseRegexFilter)
{
return TreePage.FilterRegex?.IsMatch(property.Name) ?? true;
}
return property.Name.IndexOf(TreePage.PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1;
}
return true;

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

@ -1,15 +1,20 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text.RegularExpressions;
using Avalonia.Controls;
using Avalonia.Controls.Selection;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
internal class TreePageViewModel : ViewModelBase, IDisposable
internal class TreePageViewModel : ViewModelBase, IDisposable, INotifyDataErrorInfo
{
private readonly Dictionary<string, string> _errors = new Dictionary<string, string>();
private TreeNode _selectedNode;
private ControlDetailsViewModel _details;
private string _propertyFilter;
private string _propertyFilter = string.Empty;
private bool _useRegexFilter;
public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes)
{
@ -26,15 +31,10 @@ namespace Avalonia.Diagnostics.ViewModels
get => _selectedNode;
private set
{
if (Details != null)
{
_propertyFilter = Details.PropertyFilter;
}
if (RaiseAndSetIfChanged(ref _selectedNode, value))
{
Details = value != null ?
new ControlDetailsViewModel(value.Visual, _propertyFilter) :
new ControlDetailsViewModel(this, value.Visual) :
null;
}
}
@ -54,6 +54,63 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public Regex FilterRegex { get; set; }
private void UpdateFilterRegex()
{
void ClearError()
{
if (_errors.Remove(nameof(PropertyFilter)))
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter)));
}
}
if (UseRegexFilter)
{
try
{
FilterRegex = new Regex(PropertyFilter, RegexOptions.Compiled);
ClearError();
}
catch (Exception exception)
{
_errors[nameof(PropertyFilter)] = exception.Message;
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter)));
}
}
else
{
ClearError();
}
}
public string PropertyFilter
{
get => _propertyFilter;
set
{
if (RaiseAndSetIfChanged(ref _propertyFilter, value))
{
UpdateFilterRegex();
Details.PropertiesView.Refresh();
}
}
}
public bool UseRegexFilter
{
get => _useRegexFilter;
set
{
if (RaiseAndSetIfChanged(ref _useRegexFilter, value))
{
UpdateFilterRegex();
Details.PropertiesView.Refresh();
}
}
}
public void Dispose()
{
foreach (var node in Nodes)
@ -130,5 +187,17 @@ namespace Avalonia.Diagnostics.ViewModels
return null;
}
public IEnumerable GetErrors(string propertyName)
{
if (_errors.TryGetValue(propertyName, out var error))
{
yield return error;
}
}
public bool HasErrors => _errors.Count > 0;
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
}
}

45
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@ -2,24 +2,31 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView">
<Grid ColumnDefinitions="*">
<DockPanel Grid.Column="0">
<TextBox DockPanel.Dock="Top"
BorderThickness="0"
Text="{Binding PropertyFilter}"
Watermark="Filter properties"/>
<DataGrid Items="{Binding PropertiesView}"
BorderThickness="0"
RowBackground="Transparent"
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
CanUserResizeColumns="true">
<DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True"/>
<DataGridTextColumn Header="Value" Binding="{Binding Value}"/>
<DataGridTextColumn Header="Type" Binding="{Binding Type}"/>
<DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,*">
<TextBox Grid.Row="0" Grid.Column="0"
BorderThickness="0"
Text="{Binding TreePage.PropertyFilter}"
Watermark="Filter properties" />
<CheckBox Grid.Row="0"
Grid.Column="1"
Content="Regex"
IsChecked="{Binding TreePage.UseRegexFilter}"/>
<DataGrid Items="{Binding PropertiesView}"
Grid.Row="1" Grid.ColumnSpan="2"
BorderThickness="0"
RowBackground="Transparent"
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
CanUserResizeColumns="true">
<DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" />
<DataGridTextColumn Header="Value" Binding="{Binding Value}" />
<DataGridTextColumn Header="Type" Binding="{Binding Type}" />
<DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>

2
src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs

@ -35,7 +35,6 @@ namespace Avalonia.Skia
IGlPlatformSurfaceRenderingSession glSession)
{
GrContext = grContext;
GrContext.PurgeResources();
_backendRenderTarget = backendRenderTarget;
_surface = surface;
_glSession = glSession;
@ -46,7 +45,6 @@ namespace Avalonia.Skia
_surface.Dispose();
_backendRenderTarget.Dispose();
GrContext.Flush();
GrContext.PurgeResources();
_glSession.Dispose();
}

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

@ -1335,6 +1335,47 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.False(model.SingleSelect);
}
[Fact]
public void Does_The_Best_It_Can_With_AutoSelecting_ViewModel()
{
// Tests the following scenario:
//
// - Items changes from empty to having 1 item
// - ViewModel auto-selects item 0 in CollectionChanged
// - SelectionModel receives CollectionChanged
// - And so adjusts the selected item from 0 to 1, which is past the end of the items.
//
// There's not much we can do about this situation because the order in which
// CollectionChanged handlers are called can't be known (the problem also exists with
// WPF). The best we can do is not select an invalid index.
var vm = new SelectionViewModel();
vm.Items.CollectionChanged += (s, e) =>
{
if (vm.SelectedIndex == -1 && vm.Items.Count > 0)
{
vm.SelectedIndex = 0;
}
};
var target = new ListBox
{
[!ListBox.ItemsProperty] = new Binding("Items"),
[!ListBox.SelectedIndexProperty] = new Binding("SelectedIndex"),
DataContext = vm,
};
Prepare(target);
vm.Items.Add("foo");
vm.Items.Add("bar");
Assert.Equal(0, target.SelectedIndex);
Assert.Equal(new[] { 0 }, target.Selection.SelectedIndexes);
Assert.Equal("foo", target.SelectedItem);
Assert.Equal(new[] { "foo" }, target.SelectedItems);
}
private static void Prepare(SelectingItemsControl target)
{
var root = new TestRoot
@ -1397,6 +1438,28 @@ namespace Avalonia.Controls.UnitTests.Primitives
public int SelectedIndex { get; set; }
}
private class SelectionViewModel : NotifyingBase
{
private int _selectedIndex = -1;
public SelectionViewModel()
{
Items = new ObservableCollection<string>();
}
public int SelectedIndex
{
get => _selectedIndex;
set
{
_selectedIndex = value;
RaisePropertyChanged();
}
}
public ObservableCollection<string> Items { get; }
}
private class RootWithItems : TestRoot
{
public List<string> Items { get; set; } = new List<string>() { "a", "b", "c", "d", "e" };

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

@ -1230,6 +1230,38 @@ namespace Avalonia.Controls.UnitTests.Selection
Assert.Equal(1, resetRaised);
Assert.Equal(1, selectedIndexRaised);
}
[Fact]
public void Handles_Selection_Made_In_CollectionChanged()
{
// Tests the following scenario:
//
// - Items changes from empty to having 2 items
// - ViewModel auto-selects range 0..1 in CollectionChanged
// - SelectionModel receives CollectionChanged
// - And so adjusts the selected item from 0..1 to 2..4, which is past the end of
// the items.
//
// There's not much we can do about this situation because the order in which
// CollectionChanged handlers are called can't be known (the problem also exists with
// WPF). The best we can do is not select an invalid index.
var target = CreateTarget(createData: false);
var data = new AvaloniaList<string>();
data.CollectionChanged += (s, e) =>
{
target.SelectRange(0, 1);
};
target.Source = data;
data.AddRange(new[] { "foo", "bar" });
Assert.Equal(0, target.SelectedIndex);
Assert.Equal(new[] { 0, 1 }, target.SelectedIndexes);
Assert.Equal("foo", target.SelectedItem);
Assert.Equal(new[] { "foo", "bar" }, target.SelectedItems);
Assert.Equal(0, target.AnchorIndex);
}
}
public class BatchUpdate

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

@ -1052,6 +1052,37 @@ namespace Avalonia.Controls.UnitTests.Selection
Assert.Equal(1, resetRaised);
Assert.Equal(1, selectedIndexRaised);
}
[Fact]
public void Handles_Selection_Made_In_CollectionChanged()
{
// Tests the following scenario:
//
// - Items changes from empty to having 1 item
// - ViewModel auto-selects item 0 in CollectionChanged
// - SelectionModel receives CollectionChanged
// - And so adjusts the selected item from 0 to 1, which is past the end of the items.
//
// There's not much we can do about this situation because the order in which
// CollectionChanged handlers are called can't be known (the problem also exists with
// WPF). The best we can do is not select an invalid index.
var target = CreateTarget(createData: false);
var data = new AvaloniaList<string>();
data.CollectionChanged += (s, e) =>
{
target.Select(0);
};
target.Source = data;
data.Add("foo");
Assert.Equal(0, target.SelectedIndex);
Assert.Equal(new[] { 0 }, target.SelectedIndexes);
Assert.Equal("foo", target.SelectedItem);
Assert.Equal(new[] { "foo" }, target.SelectedItems);
Assert.Equal(0, target.AnchorIndex);
}
}
public class BatchUpdate

Loading…
Cancel
Save