diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index 59b5bf48a5..aa62ae3e71 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
-using System.Diagnostics.CodeAnalysis;
using Avalonia.Automation.Peers;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
@@ -17,7 +16,6 @@ using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Styling;
-using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@@ -91,10 +89,11 @@ namespace Avalonia.Controls
/// Gets or sets the to use for binding to the display member of each item.
///
[AssignBinding]
+ [InheritDataTypeFromItems(nameof(Items))]
public IBinding? DisplayMemberBinding
{
- get { return GetValue(DisplayMemberBindingProperty); }
- set { SetValue(DisplayMemberBindingProperty, value); }
+ get => GetValue(DisplayMemberBindingProperty);
+ set => SetValue(DisplayMemberBindingProperty, value);
}
private IEnumerable? _items = new AvaloniaList();
@@ -134,8 +133,8 @@ namespace Avalonia.Controls
[Content]
public IEnumerable? Items
{
- get { return _items; }
- set { SetAndRaise(ItemsProperty, ref _items, value); }
+ get => _items;
+ set => SetAndRaise(ItemsProperty, ref _items, value);
}
///
@@ -143,8 +142,8 @@ namespace Avalonia.Controls
///
public ControlTheme? ItemContainerTheme
{
- get { return GetValue(ItemContainerThemeProperty); }
- set { SetValue(ItemContainerThemeProperty, value); }
+ get => GetValue(ItemContainerThemeProperty);
+ set => SetValue(ItemContainerThemeProperty, value);
}
///
@@ -161,8 +160,8 @@ namespace Avalonia.Controls
///
public ITemplate ItemsPanel
{
- get { return GetValue(ItemsPanelProperty); }
- set { SetValue(ItemsPanelProperty, value); }
+ get => GetValue(ItemsPanelProperty);
+ set => SetValue(ItemsPanelProperty, value);
}
///
@@ -171,8 +170,8 @@ namespace Avalonia.Controls
[InheritDataTypeFromItems(nameof(Items))]
public IDataTemplate? ItemTemplate
{
- get { return GetValue(ItemTemplateProperty); }
- set { SetValue(ItemTemplateProperty, value); }
+ get => GetValue(ItemTemplateProperty);
+ set => SetValue(ItemTemplateProperty, value);
}
///
@@ -264,8 +263,8 @@ namespace Avalonia.Controls
///
public bool AreHorizontalSnapPointsRegular
{
- get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
- set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
+ get => GetValue(AreHorizontalSnapPointsRegularProperty);
+ set => SetValue(AreHorizontalSnapPointsRegularProperty, value);
}
///
@@ -273,8 +272,8 @@ namespace Avalonia.Controls
///
public bool AreVerticalSnapPointsRegular
{
- get { return GetValue(AreVerticalSnapPointsRegularProperty); }
- set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
+ get => GetValue(AreVerticalSnapPointsRegularProperty);
+ set => SetValue(AreVerticalSnapPointsRegularProperty, value);
}
///
diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index 5210362505..eb39e92cbe 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
@@ -4,15 +4,14 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
-using System.Xml.Linq;
-using Avalonia.Controls.Generators;
using Avalonia.Controls.Selection;
+using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
+using Avalonia.Metadata;
using Avalonia.Threading;
-using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
@@ -66,6 +65,19 @@ namespace Avalonia.Controls.Primitives
(o, v) => o.SelectedItem = v,
defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty SelectedValueProperty =
+ AvaloniaProperty.Register(nameof(SelectedValue),
+ defaultBindingMode: BindingMode.TwoWay);
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty SelectedValueBindingProperty =
+ AvaloniaProperty.Register(nameof(SelectedValueBinding));
+
///
/// Defines the property.
///
@@ -129,6 +141,8 @@ namespace Avalonia.Controls.Primitives
private bool _ignoreContainerSelectionChanged;
private UpdateState? _updateState;
private bool _hasScrolledToSelectedItem;
+ private BindingHelper? _bindingHelper;
+ private bool _isSelectionChangeActive;
///
/// Initializes static members of the class.
@@ -143,8 +157,8 @@ namespace Avalonia.Controls.Primitives
///
public event EventHandler? SelectionChanged
{
- add { AddHandler(SelectionChangedEvent, value); }
- remove { RemoveHandler(SelectionChangedEvent, value); }
+ add => AddHandler(SelectionChangedEvent, value);
+ remove => RemoveHandler(SelectionChangedEvent, value);
}
///
@@ -152,8 +166,8 @@ namespace Avalonia.Controls.Primitives
///
public bool AutoScrollToSelectedItem
{
- get { return GetValue(AutoScrollToSelectedItemProperty); }
- set { SetValue(AutoScrollToSelectedItemProperty, value); }
+ get => GetValue(AutoScrollToSelectedItemProperty);
+ set => SetValue(AutoScrollToSelectedItemProperty, value);
}
///
@@ -209,6 +223,28 @@ namespace Avalonia.Controls.Primitives
}
}
+ ///
+ /// Gets the instance used to obtain the
+ /// property
+ ///
+ [AssignBinding]
+ [InheritDataTypeFromItems(nameof(Items))]
+ public IBinding? SelectedValueBinding
+ {
+ get => GetValue(SelectedValueBindingProperty);
+ set => SetValue(SelectedValueBindingProperty, value);
+ }
+
+ ///
+ /// Gets or sets the value of the selected item, obtained using
+ ///
+ ///
+ public object? SelectedValue
+ {
+ get => GetValue(SelectedValueProperty);
+ set => SetValue(SelectedValueProperty, value);
+ }
+
///
/// Gets or sets the selected items.
///
@@ -322,8 +358,8 @@ namespace Avalonia.Controls.Primitives
///
public bool IsTextSearchEnabled
{
- get { return GetValue(IsTextSearchEnabledProperty); }
- set { SetValue(IsTextSearchEnabledProperty, value); }
+ get => GetValue(IsTextSearchEnabledProperty);
+ set => SetValue(IsTextSearchEnabledProperty, value);
}
///
@@ -332,8 +368,8 @@ namespace Avalonia.Controls.Primitives
///
public bool WrapSelection
{
- get { return GetValue(WrapSelectionProperty); }
- set { SetValue(WrapSelectionProperty, value); }
+ get => GetValue(WrapSelectionProperty);
+ set => SetValue(WrapSelectionProperty, value);
}
///
@@ -345,8 +381,8 @@ namespace Avalonia.Controls.Primitives
///
protected SelectionMode SelectionMode
{
- get { return GetValue(SelectionModeProperty); }
- set { SetValue(SelectionModeProperty, value); }
+ get => GetValue(SelectionModeProperty);
+ set => SetValue(SelectionModeProperty, value);
}
///
@@ -609,6 +645,60 @@ namespace Avalonia.Controls.Primitives
{
WrapFocus = WrapSelection;
}
+ else if (change.Property == SelectedValueProperty)
+ {
+ if (_isSelectionChangeActive)
+ return;
+
+ if (_updateState is not null)
+ {
+ _updateState.SelectedValue = change.NewValue;
+ return;
+ }
+
+ SelectItemWithValue(change.NewValue);
+ }
+ else if (change.Property == SelectedValueBindingProperty)
+ {
+ var idx = SelectedIndex;
+
+ // If no selection is active, don't do anything as SelectedValue is already null
+ if (idx == -1)
+ {
+ return;
+ }
+
+ var value = change.GetNewValue();
+ if (value is null)
+ {
+ // Clearing SelectedValueBinding makes the SelectedValue the item itself
+ SelectedValue = SelectedItem;
+ return;
+ }
+
+ var selectedItem = SelectedItem;
+
+ try
+ {
+ _isSelectionChangeActive = true;
+
+ if (_bindingHelper is null)
+ {
+ _bindingHelper = new BindingHelper(value);
+ }
+ else
+ {
+ _bindingHelper.UpdateBinding(value);
+ }
+
+ // Re-evaluate SelectedValue with the new binding
+ SelectedValue = _bindingHelper.Evaluate(selectedItem);
+ }
+ finally
+ {
+ _isSelectionChangeActive = false;
+ }
+ }
}
///
@@ -815,6 +905,10 @@ namespace Avalonia.Controls.Primitives
new BindingValue(SelectedItems));
_oldSelectedItems = SelectedItems;
}
+ else if (e.PropertyName == nameof(ISelectionModel.Source))
+ {
+ ClearValue(SelectedValueProperty);
+ }
}
///
@@ -845,6 +939,11 @@ namespace Avalonia.Controls.Primitives
Mark(i, false);
}
+ if (!_isSelectionChangeActive)
+ {
+ UpdateSelectedValueFromItem();
+ }
+
var route = BuildEventRoute(SelectionChangedEvent);
if (route.HasHandlers)
@@ -871,6 +970,109 @@ namespace Avalonia.Controls.Primitives
}
}
+ private void SelectItemWithValue(object? value)
+ {
+ if (ItemCount == 0 || _isSelectionChangeActive)
+ return;
+
+ try
+ {
+ _isSelectionChangeActive = true;
+ var si = FindItemWithValue(value);
+ if (si != AvaloniaProperty.UnsetValue)
+ {
+ SelectedItem = si;
+ }
+ else
+ {
+ SelectedItem = null;
+ }
+ }
+ finally
+ {
+ _isSelectionChangeActive = false;
+ }
+ }
+
+ private object FindItemWithValue(object? value)
+ {
+ if (ItemCount == 0 || value is null)
+ {
+ return AvaloniaProperty.UnsetValue;
+ }
+
+ var items = Items;
+ var binding = SelectedValueBinding;
+
+ if (binding is null)
+ {
+ // No SelectedValueBinding set, SelectedValue is the item itself
+ // Still verify the value passed in is in the Items list
+ var index = items!.IndexOf(value);
+
+ if (index >= 0)
+ {
+ return value;
+ }
+ else
+ {
+ return AvaloniaProperty.UnsetValue;
+ }
+ }
+
+ _bindingHelper ??= new BindingHelper(binding);
+
+ // Matching UWP behavior, if duplicates are present, return the first item matching
+ // the SelectedValue provided
+ foreach (var item in items!)
+ {
+ var itemValue = _bindingHelper.Evaluate(item);
+
+ if (itemValue.Equals(value))
+ {
+ return item;
+ }
+ }
+
+ return AvaloniaProperty.UnsetValue;
+ }
+
+ private void UpdateSelectedValueFromItem()
+ {
+ if (_isSelectionChangeActive)
+ return;
+
+ var binding = SelectedValueBinding;
+ var item = SelectedItem;
+
+ if (binding is null || item is null)
+ {
+ // No SelectedValueBinding, SelectedValue is Item itself
+ try
+ {
+ _isSelectionChangeActive = true;
+ SelectedValue = item;
+ }
+ finally
+ {
+ _isSelectionChangeActive = false;
+ }
+ return;
+ }
+
+ _bindingHelper ??= new BindingHelper(binding);
+
+ try
+ {
+ _isSelectionChangeActive = true;
+ SelectedValue = _bindingHelper.Evaluate(item);
+ }
+ finally
+ {
+ _isSelectionChangeActive = false;
+ }
+ }
+
private void AutoScrollToSelectedItemIfNecessary()
{
if (AutoScrollToSelectedItem &&
@@ -1037,6 +1239,13 @@ namespace Avalonia.Controls.Primitives
Selection.Clear();
}
+ if (state.SelectedValue.HasValue)
+ {
+ var item = FindItemWithValue(state.SelectedValue.Value);
+ if (item != AvaloniaProperty.UnsetValue)
+ state.SelectedItem = item;
+ }
+
if (state.SelectedIndex.HasValue)
{
SelectedIndex = state.SelectedIndex.Value;
@@ -1098,6 +1307,7 @@ namespace Avalonia.Controls.Primitives
{
private Optional _selectedIndex;
private Optional _selectedItem;
+ private Optional _selectedValue;
public int UpdateCount { get; set; }
public Optional Selection { get; set; }
@@ -1122,6 +1332,54 @@ namespace Avalonia.Controls.Primitives
_selectedIndex = default;
}
}
+
+ public Optional SelectedValue
+ {
+ get => _selectedValue;
+ set
+ {
+ _selectedValue = value;
+ }
+ }
+ }
+
+ ///
+ /// Helper class for evaluating a binding from an Item and IBinding instance
+ ///
+ private class BindingHelper : StyledElement
+ {
+ public BindingHelper(IBinding binding)
+ {
+ UpdateBinding(binding);
+ }
+
+ public static readonly StyledProperty ValueProperty =
+ AvaloniaProperty.Register("Value");
+
+ public object Evaluate(object? dataContext)
+ {
+ dataContext = dataContext ?? throw new ArgumentNullException(nameof(dataContext));
+
+ // Only update the DataContext if necessary
+ if (!dataContext.Equals(DataContext))
+ DataContext = dataContext;
+
+ return GetValue(ValueProperty);
+ }
+
+ public void UpdateBinding(IBinding binding)
+ {
+ _lastBinding = binding;
+ var ib = binding.Initiate(this, ValueProperty);
+ if (ib is null)
+ {
+ throw new InvalidOperationException("Unable to create binding");
+ }
+
+ BindingOperations.Apply(this, ValueProperty, ib, null);
+ }
+
+ private IBinding? _lastBinding;
}
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs
new file mode 100644
index 0000000000..df81b1faae
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs
@@ -0,0 +1,330 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.Data;
+using Avalonia.Styling;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests.Primitives
+{
+ public class SelectingItemsControlTests_SelectedValue
+ {
+ [Fact]
+ public void Setting_SelectedItem_Sets_SelectedValue()
+ {
+ var items = TestClass.GetItems();
+ var sic = new SelectingItemsControl
+ {
+ Items = items,
+ SelectedValueBinding = new Binding("Name"),
+ Template = Template()
+ };
+
+ sic.SelectedItem = items[0];
+
+ Assert.Equal(items[0].Name, sic.SelectedValue);
+ }
+
+ [Fact]
+ public void Setting_SelectedIndex_Sets_SelectedValue()
+ {
+ var items = TestClass.GetItems();
+ var sic = new SelectingItemsControl
+ {
+ Items = items,
+ SelectedValueBinding = new Binding("Name"),
+ Template = Template()
+ };
+
+ sic.SelectedIndex = 0;
+
+ Assert.Equal(items[0].Name, sic.SelectedValue);
+ }
+
+ [Fact]
+ public void Setting_SelectedItems_Sets_SelectedValue()
+ {
+ var items = TestClass.GetItems();
+ var sic = new ListBox
+ {
+ Items = items,
+ SelectedValueBinding = new Binding("Name"),
+ Template = Template()
+ };
+
+ sic.SelectedItems = new List
+ {
+ items[1],
+ items[3],
+ items[4]
+ };
+
+ // When interacting, SelectedItem is the first item in the SelectedItems collection
+ // But when set here, it's the last
+ Assert.Equal(items[4].Name, sic.SelectedValue);
+ }
+
+ [Fact]
+ public void Setting_SelectedValue_Sets_SelectedIndex()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var items = TestClass.GetItems();
+ var sic = new SelectingItemsControl
+ {
+ Items = items,
+ SelectedValueBinding = new Binding("Name"),
+ Template = Template()
+ };
+
+ Prepare(sic);
+
+ sic.SelectedValue = items[1].Name;
+
+ Assert.Equal(1, sic.SelectedIndex);
+ }
+ }
+
+ [Fact]
+ public void Setting_SelectedValue_Sets_SelectedItem()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var items = TestClass.GetItems();
+ var sic = new SelectingItemsControl
+ {
+ Items = items,
+ SelectedValueBinding = new Binding("Name"),
+ Template = Template()
+ };
+
+ Prepare(sic);
+
+ sic.SelectedValue = "Item2";
+
+ Assert.Equal(items[1], sic.SelectedItem);
+ }
+ }
+
+ [Fact]
+ public void Changing_SelectedValueBinding_Updates_SelectedValue()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var items = TestClass.GetItems();
+ var sic = new SelectingItemsControl
+ {
+ Items = items,
+ SelectedValueBinding = new Binding("Name"),
+ Template = Template()
+ };
+
+ sic.SelectedValue = "Item2";
+
+ sic.SelectedValueBinding = new Binding("AltProperty");
+
+ // Ensure SelectedItem didn't change
+ Assert.Equal(items[1], sic.SelectedItem);
+
+
+ Assert.Equal("Alt2", sic.SelectedValue);
+ }
+ }
+
+ [Fact]
+ public void SelectedValue_With_Null_SelectedValueBinding_Is_Item()
+ {
+ var items = TestClass.GetItems();
+ var sic = new SelectingItemsControl
+ {
+ Items = items,
+ Template = Template()
+ };
+
+ sic.SelectedIndex = 0;
+
+ Assert.Equal(items[0], sic.SelectedValue);
+ }
+
+ [Fact]
+ public void Setting_SelectedValue_Before_Initialize_Should_Retain_Selection()
+ {
+ var items = TestClass.GetItems();
+ var sic = new SelectingItemsControl
+ {
+ Items = items,
+ Template = Template(),
+ SelectedValueBinding = new Binding("Name"),
+ SelectedValue = "Item2"
+ };
+
+ sic.BeginInit();
+ sic.EndInit();
+
+ Assert.Equal(items[1].Name, sic.SelectedValue);
+ }
+
+ [Fact]
+ public void Setting_SelectedValue_During_Initialize_Should_Take_Priority_Over_Previous_Value()
+ {
+ var items = TestClass.GetItems();
+ var sic = new SelectingItemsControl
+ {
+ Items = items,
+ Template = Template(),
+ SelectedValueBinding = new Binding("Name"),
+ SelectedValue = "Item2"
+ };
+
+ sic.BeginInit();
+ sic.SelectedValue = "Item1";
+ sic.EndInit();
+
+ Assert.Equal(items[0].Name, sic.SelectedValue);
+ }
+
+ [Fact]
+ public void Changing_Items_Should_Clear_SelectedValue()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var items = TestClass.GetItems();
+ var sic = new SelectingItemsControl
+ {
+ Items = items,
+ Template = Template(),
+ SelectedValueBinding = new Binding("Name"),
+ SelectedValue = "Item2"
+ };
+
+ Prepare(sic);
+
+ sic.Items = new List
+ {
+ new TestClass("NewItem", string.Empty)
+ };
+
+ Assert.Equal(null, sic.SelectedValue);
+ }
+ }
+
+ [Fact]
+ public void Setting_SelectedValue_Should_Raise_SelectionChanged_Event()
+ {
+ // Unlike SelectedIndex/SelectedItem tests, we need the ItemsControl to
+ // initialize so that SelectedValue can actually be looked up
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var items = TestClass.GetItems();
+ var sic = new SelectingItemsControl
+ {
+ Items = items,
+ Template = Template(),
+ SelectedValueBinding = new Binding("Name"),
+ };
+
+ Prepare(sic);
+
+ var called = false;
+ sic.SelectionChanged += (s, e) =>
+ {
+ Assert.Same(items[1], e.AddedItems.Cast().Single());
+ Assert.Empty(e.RemovedItems);
+ called = true;
+ };
+
+ sic.SelectedValue = "Item2";
+ Assert.True(called);
+ }
+ }
+
+ [Fact]
+ public void Clearing_SelectedValue_Should_Raise_SelectionChanged_Event()
+ {
+ var items = TestClass.GetItems();
+ var sic = new SelectingItemsControl
+ {
+ Items = items,
+ Template = Template(),
+ SelectedValueBinding = new Binding("Name"),
+ SelectedValue = "Item2"
+ };
+
+ var called = false;
+ sic.SelectionChanged += (s, e) =>
+ {
+ Assert.Same(items[1], e.RemovedItems.Cast().Single());
+ Assert.Empty(e.AddedItems);
+ called = true;
+ };
+
+ sic.SelectedValue = null;
+ Assert.True(called);
+ }
+
+ private static FuncControlTemplate Template()
+ {
+ return new FuncControlTemplate((control, scope) =>
+ new ItemsPresenter
+ {
+ Name = "itemsPresenter",
+ [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty],
+ }.RegisterInNameScope(scope));
+ }
+
+ private static void Prepare(SelectingItemsControl target)
+ {
+ var root = new TestRoot
+ {
+ Child = target,
+ Width = 100,
+ Height = 100,
+ Styles =
+ {
+ new Style(x => x.Is())
+ {
+ Setters =
+ {
+ new Setter(ListBox.TemplateProperty, Template()),
+ },
+ },
+ },
+ };
+
+ root.LayoutManager.ExecuteInitialLayoutPass();
+ }
+ }
+
+ internal class TestClass
+ {
+ public TestClass(string name, string alt)
+ {
+ Name = name;
+ AltProperty = alt;
+ }
+
+ public string Name { get; set; }
+
+ public string AltProperty { get; set; }
+
+ public static List GetItems()
+ {
+ return new List
+ {
+ new TestClass("Item1", "Alt1"),
+ new TestClass("Item2", "Alt2"),
+ new TestClass("Item3", "Alt3"),
+ new TestClass("Item4", "Alt4"),
+ new TestClass("Item5", "Alt5"),
+ };
+ }
+ }
+}
+
+