// Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Data; using Avalonia.UnitTests; using Moq; using Xunit; namespace Avalonia.Controls.UnitTests.Primitives { public class SelectingItemsControlTests { private MouseTestHelper _helper = new MouseTestHelper(); [Fact] public void SelectedIndex_Should_Initially_Be_Minus_1() { var items = new[] { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; Assert.Equal(-1, target.SelectedIndex); } [Fact] public void Item_IsSelected_Should_Initially_Be_False() { var items = new[] { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.ApplyTemplate(); Assert.False(items[0].IsSelected); Assert.False(items[1].IsSelected); } [Fact] public void Setting_SelectedItem_Should_Set_Item_IsSelected_True() { var items = new[] { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); target.SelectedItem = items[1]; Assert.False(items[0].IsSelected); Assert.True(items[1].IsSelected); } [Fact] public void Setting_SelectedItem_Before_ApplyTemplate_Should_Set_Item_IsSelected_True() { var items = new[] { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.SelectedItem = items[1]; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); Assert.False(items[0].IsSelected); Assert.True(items[1].IsSelected); } [Fact] public void Setting_SelectedIndex_Before_ApplyTemplate_Should_Set_Item_IsSelected_True() { var items = new[] { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.SelectedIndex = 1; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); Assert.False(items[0].IsSelected); Assert.True(items[1].IsSelected); } [Fact] public void Setting_SelectedItem_Should_Set_SelectedIndex() { var items = new[] { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.ApplyTemplate(); target.SelectedItem = items[1]; Assert.Equal(items[1], target.SelectedItem); Assert.Equal(1, target.SelectedIndex); } [Fact] public void SelectedIndex_Item_Is_Updated_As_Items_Removed_When_Last_Item_Is_Selected() { var items = new ObservableCollection { "Foo", "Bar", "FooBar" }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.ApplyTemplate(); target.SelectedItem = items[2]; Assert.Equal(items[2], target.SelectedItem); Assert.Equal(2, target.SelectedIndex); items.RemoveAt(0); Assert.Equal(items[1], target.SelectedItem); Assert.Equal(1, target.SelectedIndex); } [Fact] public void Setting_SelectedItem_To_Not_Present_Item_Should_Clear_Selection() { var items = new[] { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.ApplyTemplate(); target.SelectedItem = items[1]; Assert.Equal(items[1], target.SelectedItem); Assert.Equal(1, target.SelectedIndex); target.SelectedItem = new Item(); Assert.Null(target.SelectedItem); Assert.Equal(-1, target.SelectedIndex); } [Fact] public void Setting_SelectedIndex_Should_Set_SelectedItem() { var items = new[] { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.ApplyTemplate(); target.SelectedIndex = 1; Assert.Equal(items[1], target.SelectedItem); } [Fact] public void Setting_SelectedIndex_Out_Of_Bounds_Should_Clear_Selection() { var items = new[] { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.ApplyTemplate(); target.SelectedIndex = 2; Assert.Equal(-1, target.SelectedIndex); } [Fact] public void Setting_SelectedItem_To_Non_Existent_Item_Should_Clear_Selection() { var target = new SelectingItemsControl { Template = Template(), }; target.ApplyTemplate(); target.SelectedItem = new Item(); Assert.Equal(-1, target.SelectedIndex); Assert.Null(target.SelectedItem); } [Fact] public void Adding_Selected_Item_Should_Update_Selection() { var items = new AvaloniaList(new[] { new Item(), new Item(), }); var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); items.Add(new Item { IsSelected = true }); Assert.Equal(2, target.SelectedIndex); Assert.Equal(items[2], target.SelectedItem); } [Fact] public void Setting_Items_To_Null_Should_Clear_Selection() { var items = new AvaloniaList { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.ApplyTemplate(); target.SelectedIndex = 1; Assert.Equal(items[1], target.SelectedItem); Assert.Equal(1, target.SelectedIndex); target.Items = null; Assert.Null(target.SelectedItem); Assert.Equal(-1, target.SelectedIndex); } [Fact] public void Removing_Selected_Item_Should_Clear_Selection() { var items = new AvaloniaList { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.ApplyTemplate(); target.SelectedIndex = 1; Assert.Equal(items[1], target.SelectedItem); Assert.Equal(1, target.SelectedIndex); items.RemoveAt(1); Assert.Null(target.SelectedItem); Assert.Equal(-1, target.SelectedIndex); } [Fact] public void Moving_Selected_Item_Should_Update_Selection() { var items = new AvaloniaList { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.ApplyTemplate(); target.SelectedIndex = 0; Assert.Equal(items[0], target.SelectedItem); Assert.Equal(0, target.SelectedIndex); items.Move(0, 1); Assert.Equal(items[1], target.SelectedItem); Assert.Equal(1, target.SelectedIndex); } [Fact] public void Resetting_Items_Collection_Should_Clear_Selection() { // Need to use ObservableCollection here as AvaloniaList signals a Clear as an // add + remove. var items = new ObservableCollection { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.ApplyTemplate(); target.SelectedIndex = 1; Assert.Equal(items[1], target.SelectedItem); Assert.Equal(1, target.SelectedIndex); items.Clear(); Assert.Null(target.SelectedItem); Assert.Equal(-1, target.SelectedIndex); } [Fact] public void Raising_IsSelectedChanged_On_Item_Should_Update_Selection() { var items = new[] { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); target.SelectedItem = items[1]; Assert.False(items[0].IsSelected); Assert.True(items[1].IsSelected); items[0].IsSelected = true; items[0].RaiseEvent(new RoutedEventArgs(SelectingItemsControl.IsSelectedChangedEvent)); Assert.Equal(0, target.SelectedIndex); Assert.Equal(items[0], target.SelectedItem); Assert.True(items[0].IsSelected); Assert.False(items[1].IsSelected); } [Fact] public void Clearing_IsSelected_And_Raising_IsSelectedChanged_On_Item_Should_Update_Selection() { var items = new[] { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); target.SelectedItem = items[1]; Assert.False(items[0].IsSelected); Assert.True(items[1].IsSelected); items[1].IsSelected = false; items[1].RaiseEvent(new RoutedEventArgs(SelectingItemsControl.IsSelectedChangedEvent)); Assert.Equal(-1, target.SelectedIndex); Assert.Null(target.SelectedItem); } [Fact] public void Raising_IsSelectedChanged_On_Someone_Elses_Item_Should_Not_Update_Selection() { var items = new[] { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; target.ApplyTemplate(); target.SelectedItem = items[1]; var notChild = new Item { IsSelected = true, }; target.RaiseEvent(new RoutedEventArgs { RoutedEvent = SelectingItemsControl.IsSelectedChangedEvent, Source = notChild, }); Assert.Equal(target.SelectedItem, items[1]); } [Fact] public void Setting_SelectedIndex_Should_Raise_SelectionChanged_Event() { var items = new[] { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), }; var called = false; target.SelectionChanged += (s, e) => { Assert.Same(items[1], e.AddedItems.Cast().Single()); Assert.Empty(e.RemovedItems); called = true; }; target.SelectedIndex = 1; Assert.True(called); } [Fact] public void Clearing_SelectedIndex_Should_Raise_SelectionChanged_Event() { var items = new[] { new Item(), new Item(), }; var target = new SelectingItemsControl { Items = items, Template = Template(), SelectedIndex = 1, }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); var called = false; target.SelectionChanged += (s, e) => { Assert.Same(items[1], e.RemovedItems.Cast().Single()); Assert.Empty(e.AddedItems); called = true; }; target.SelectedIndex = -1; Assert.True(called); } [Fact] public void Order_Of_Setting_Items_And_SelectedIndex_During_Initialization_Should_Not_Matter() { var items = new[] { "Foo", "Bar" }; var target = new SelectingItemsControl(); ((ISupportInitialize)target).BeginInit(); target.SelectedIndex = 1; target.Items = items; ((ISupportInitialize)target).EndInit(); Assert.Equal(1, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); } [Fact] public void Order_Of_Setting_Items_And_SelectedItem_During_Initialization_Should_Not_Matter() { var items = new[] { "Foo", "Bar" }; var target = new SelectingItemsControl(); ((ISupportInitialize)target).BeginInit(); target.SelectedItem = "Bar"; target.Items = items; ((ISupportInitialize)target).EndInit(); Assert.Equal(1, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); } [Fact] public void Changing_DataContext_Should_Not_Clear_Nested_ViewModel_SelectedItem() { var items = new[] { new Item(), new Item(), }; var vm = new MasterViewModel { Child = new ChildViewModel { Items = items, SelectedItem = items[1], } }; var target = new SelectingItemsControl { DataContext = vm }; var itemsBinding = new Binding("Child.Items"); var selectedBinding = new Binding("Child.SelectedItem"); target.Bind(SelectingItemsControl.ItemsProperty, itemsBinding); target.Bind(SelectingItemsControl.SelectedItemProperty, selectedBinding); Assert.Equal(1, target.SelectedIndex); Assert.Same(vm.Child.SelectedItem, target.SelectedItem); items = new[] { new Item { Value = "Item1" }, new Item { Value = "Item2" }, new Item { Value = "Item3" }, }; vm = new MasterViewModel { Child = new ChildViewModel { Items = items, SelectedItem = items[2], } }; target.DataContext = vm; Assert.Equal(2, target.SelectedIndex); Assert.Same(vm.Child.SelectedItem, target.SelectedItem); } [Fact] public void Nested_ListBox_Does_Not_Change_Parent_SelectedIndex() { SelectingItemsControl nested; var root = new SelectingItemsControl { Template = Template(), Items = new IControl[] { new Border(), nested = new ListBox { Template = Template(), Items = new[] { "foo", "bar" }, SelectedIndex = 1, } }, SelectedIndex = 0, }; root.ApplyTemplate(); root.Presenter.ApplyTemplate(); nested.ApplyTemplate(); nested.Presenter.ApplyTemplate(); Assert.Equal(0, root.SelectedIndex); Assert.Equal(1, nested.SelectedIndex); nested.SelectedIndex = 0; Assert.Equal(0, root.SelectedIndex); } [Fact] public void Setting_SelectedItem_With_Pointer_Should_Set_TabOnceActiveElement() { var target = new ListBox { Template = Template(), Items = new[] { "Foo", "Bar", "Baz " }, }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); _helper.Down((Interactive)target.Presenter.Panel.Children[1]); var panel = target.Presenter.Panel; Assert.Equal( KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel), panel.Children[1]); } [Fact] public void Removing_SelectedItem_Should_Clear_TabOnceActiveElement() { var items = new ObservableCollection(new[] { "Foo", "Bar", "Baz " }); var target = new ListBox { Template = Template(), Items = items, }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); _helper.Down(target.Presenter.Panel.Children[1]); items.RemoveAt(1); var panel = target.Presenter.Panel; Assert.Null(KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel)); } [Fact] public void Resetting_Items_Collection_Should_Retain_Selection() { var itemsMock = new Mock>(); var itemsMockAsINCC = itemsMock.As(); itemsMock.Object.AddRange(new[] { "Foo", "Bar", "Baz" }); var target = new SelectingItemsControl { Items = itemsMock.Object }; target.SelectedIndex = 1; itemsMockAsINCC.Raise(e => e.CollectionChanged += null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); Assert.True(target.SelectedIndex == 1); } [Fact] public void Binding_With_DelayedBinding_And_Initialization_Where_DataContext_Is_Root_Works() { // Test for #1932. var root = new RootWithItems(); root.BeginInit(); root.DataContext = root; var target = new ListBox(); target.BeginInit(); root.Child = target; DelayedBinding.Add(target, ItemsControl.ItemsProperty, new Binding(nameof(RootWithItems.Items))); DelayedBinding.Add(target, ListBox.SelectedItemProperty, new Binding(nameof(RootWithItems.Selected))); target.EndInit(); root.EndInit(); Assert.Equal("b", target.SelectedItem); } [Fact] public void Mode_For_SelectedIndex_Is_TwoWay_By_Default() { var items = new[] { new Item(), new Item(), new Item(), }; var vm = new MasterViewModel { Child = new ChildViewModel { Items = items, SelectedIndex = 1, } }; var target = new SelectingItemsControl { DataContext = vm }; var itemsBinding = new Binding("Child.Items"); var selectedIndBinding = new Binding("Child.SelectedIndex"); target.Bind(SelectingItemsControl.ItemsProperty, itemsBinding); target.Bind(SelectingItemsControl.SelectedIndexProperty, selectedIndBinding); Assert.Equal(1, target.SelectedIndex); target.SelectedIndex = 2; Assert.Equal(2, target.SelectedIndex); Assert.Equal(2, vm.Child.SelectedIndex); } [Fact] public void Should_Select_Correct_Item_When_Duplicate_Items_Are_Present() { var target = new ListBox { Template = Template(), Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz"}, }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); _helper.Down((Interactive)target.Presenter.Panel.Children[3]); Assert.Equal(3, target.SelectedIndex); } [Fact] public void Should_Apply_Selected_Pseudoclass_To_Correct_Item_When_Duplicate_Items_Are_Present() { var target = new ListBox { Template = Template(), Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); _helper.Down((Interactive)target.Presenter.Panel.Children[3]); Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[3].Classes); } [Fact] public void Adding_Item_Before_SelectedItem_Should_Update_SelectedIndex() { var items = new ObservableCollection { "Foo", "Bar", "Baz" }; var target = new ListBox { Template = Template(), Items = items, SelectedIndex = 1, }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); items.Insert(0, "Qux"); Assert.Equal(2, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); } [Fact] public void Removing_Item_Before_SelectedItem_Should_Update_SelectedIndex() { var items = new ObservableCollection { "Foo", "Bar", "Baz" }; var target = new ListBox { Template = Template(), Items = items, SelectedIndex = 1, }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); items.RemoveAt(0); Assert.Equal(0, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); } [Fact] public void Replacing_Selected_Item_Should_Update_SelectedItem() { var items = new ObservableCollection { "Foo", "Bar", "Baz" }; var target = new ListBox { Template = Template(), Items = items, SelectedIndex = 1, }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); items[1] = "Qux"; Assert.Equal(1, target.SelectedIndex); Assert.Equal("Qux", target.SelectedItem); } private FuncControlTemplate Template() { return new FuncControlTemplate((control, scope) => new ItemsPresenter { Name = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], }.RegisterInNameScope(scope).WithNameScope(scope)); } private class Item : Control, ISelectable { public string Value { get; set; } public bool IsSelected { get; set; } } private class MasterViewModel : NotifyingBase { private ChildViewModel _child; public ChildViewModel Child { get { return _child; } set { _child = value; RaisePropertyChanged(); } } } private class ChildViewModel : NotifyingBase { public IList Items { get; set; } public Item SelectedItem { get; set; } public int SelectedIndex { get; set; } } private class RootWithItems : TestRoot { public List Items { get; set; } = new List() { "a", "b", "c", "d", "e" }; public string Selected { get; set; } = "b"; } } }