diff --git a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
index f5c135459d..aeb71d16ae 100644
--- a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
+++ b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
@@ -30,7 +30,7 @@ namespace Avalonia.Data.Converters
{
if (value == null)
{
- return targetType.IsValueType ? AvaloniaProperty.UnsetValue : null;
+ return null;
}
if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1)
diff --git a/src/Avalonia.Controls/Automation/Peers/ProgressBarAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ProgressBarAutomationPeer.cs
new file mode 100644
index 0000000000..3c59f74c90
--- /dev/null
+++ b/src/Avalonia.Controls/Automation/Peers/ProgressBarAutomationPeer.cs
@@ -0,0 +1,62 @@
+using System;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls.Primitives;
+
+namespace Avalonia.Controls.Automation.Peers
+{
+ public class ProgressBarAutomationPeer : RangeBaseAutomationPeer, IRangeValueProvider
+ {
+ public ProgressBarAutomationPeer(RangeBase owner) : base(owner)
+ {
+ }
+
+ protected override string GetClassNameCore()
+ {
+ return "ProgressBar";
+ }
+
+ protected override AutomationControlType GetAutomationControlTypeCore()
+ {
+ return AutomationControlType.ProgressBar;
+ }
+
+ ///
+ /// Request to set the value that this UI element is representing
+ ///
+ /// Value to set the UI to, as an object
+ /// true if the UI element was successfully set to the specified value
+ void IRangeValueProvider.SetValue(double val)
+ {
+ throw new InvalidOperationException("ProgressBar is ReadOnly, value can't be set.");
+ }
+
+ ///Indicates that the value can only be read, not modified.
+ ///returns True if the control is read-only
+ bool IRangeValueProvider.IsReadOnly
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ ///Value of a Large Change
+ double IRangeValueProvider.LargeChange
+ {
+ get
+ {
+ return double.NaN;
+ }
+ }
+
+ ///Value of a Small Change
+ double IRangeValueProvider.SmallChange
+ {
+ get
+ {
+ return double.NaN;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index 42674cb481..2f02a48d55 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -89,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();
@@ -131,8 +132,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);
}
///
@@ -140,8 +141,8 @@ namespace Avalonia.Controls
///
public ControlTheme? ItemContainerTheme
{
- get { return GetValue(ItemContainerThemeProperty); }
- set { SetValue(ItemContainerThemeProperty, value); }
+ get => GetValue(ItemContainerThemeProperty);
+ set => SetValue(ItemContainerThemeProperty, value);
}
///
@@ -158,8 +159,8 @@ namespace Avalonia.Controls
///
public ITemplate ItemsPanel
{
- get { return GetValue(ItemsPanelProperty); }
- set { SetValue(ItemsPanelProperty, value); }
+ get => GetValue(ItemsPanelProperty);
+ set => SetValue(ItemsPanelProperty, value);
}
///
@@ -168,8 +169,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);
}
///
@@ -263,8 +264,8 @@ namespace Avalonia.Controls
///
public bool AreHorizontalSnapPointsRegular
{
- get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
- set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
+ get => GetValue(AreHorizontalSnapPointsRegularProperty);
+ set => SetValue(AreHorizontalSnapPointsRegularProperty, value);
}
///
@@ -272,8 +273,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 8d79d62dc4..065c4ff2e5 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
@@ -6,10 +6,12 @@ using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
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;
namespace Avalonia.Controls.Primitives
@@ -64,6 +66,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.
///
@@ -127,6 +142,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.
@@ -141,8 +158,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);
}
///
@@ -150,8 +167,8 @@ namespace Avalonia.Controls.Primitives
///
public bool AutoScrollToSelectedItem
{
- get { return GetValue(AutoScrollToSelectedItemProperty); }
- set { SetValue(AutoScrollToSelectedItemProperty, value); }
+ get => GetValue(AutoScrollToSelectedItemProperty);
+ set => SetValue(AutoScrollToSelectedItemProperty, value);
}
///
@@ -207,6 +224,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.
///
@@ -321,8 +360,8 @@ namespace Avalonia.Controls.Primitives
///
public bool IsTextSearchEnabled
{
- get { return GetValue(IsTextSearchEnabledProperty); }
- set { SetValue(IsTextSearchEnabledProperty, value); }
+ get => GetValue(IsTextSearchEnabledProperty);
+ set => SetValue(IsTextSearchEnabledProperty, value);
}
///
@@ -331,8 +370,8 @@ namespace Avalonia.Controls.Primitives
///
public bool WrapSelection
{
- get { return GetValue(WrapSelectionProperty); }
- set { SetValue(WrapSelectionProperty, value); }
+ get => GetValue(WrapSelectionProperty);
+ set => SetValue(WrapSelectionProperty, value);
}
///
@@ -344,8 +383,8 @@ namespace Avalonia.Controls.Primitives
///
protected SelectionMode SelectionMode
{
- get { return GetValue(SelectionModeProperty); }
- set { SetValue(SelectionModeProperty, value); }
+ get => GetValue(SelectionModeProperty);
+ set => SetValue(SelectionModeProperty, value);
}
///
@@ -618,6 +657,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;
+ }
+ }
}
///
@@ -822,6 +915,10 @@ namespace Avalonia.Controls.Primitives
new BindingValue(SelectedItems));
_oldSelectedItems = SelectedItems;
}
+ else if (e.PropertyName == nameof(ISelectionModel.Source))
+ {
+ ClearValue(SelectedValueProperty);
+ }
}
///
@@ -852,6 +949,11 @@ namespace Avalonia.Controls.Primitives
Mark(i, false);
}
+ if (!_isSelectionChangeActive)
+ {
+ UpdateSelectedValueFromItem();
+ }
+
var route = BuildEventRoute(SelectionChangedEvent);
if (route.HasHandlers)
@@ -878,6 +980,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 &&
@@ -1044,6 +1249,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;
@@ -1105,6 +1317,7 @@ namespace Avalonia.Controls.Primitives
{
private Optional _selectedIndex;
private Optional _selectedItem;
+ private Optional _selectedValue;
public int UpdateCount { get; set; }
public Optional Selection { get; set; }
@@ -1129,6 +1342,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/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs
index dfb436a55e..158c5d875b 100644
--- a/src/Avalonia.Controls/Primitives/ToggleButton.cs
+++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs
@@ -20,7 +20,7 @@ namespace Avalonia.Controls.Primitives
nameof(IsChecked),
o => o.IsChecked,
(o, v) => o.IsChecked = v,
- unsetValue: null,
+ unsetValue: false,
defaultBindingMode: BindingMode.TwoWay);
///
diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs
index ce3158b282..98a9ec60bf 100644
--- a/src/Avalonia.Controls/ProgressBar.cs
+++ b/src/Avalonia.Controls/ProgressBar.cs
@@ -1,4 +1,6 @@
using System;
+using Avalonia.Automation.Peers;
+using Avalonia.Controls.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
@@ -228,6 +230,11 @@ namespace Avalonia.Controls
UpdateIndicator();
}
+ protected override AutomationPeer OnCreateAutomationPeer()
+ {
+ return new ProgressBarAutomationPeer(this);
+ }
+
private void UpdateIndicator()
{
// Gets the size of the parent indicator container
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
index 0140281d50..785fd49983 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
@@ -115,7 +115,7 @@ namespace Avalonia.Diagnostics.ViewModels
var link = _currentEvent.EventChain[linkIndex];
link.Handled = true;
- _currentEvent.HandledBy = link;
+ _currentEvent.HandledBy ??= link;
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
index cd2e92914a..f62d8a0b79 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
@@ -29,6 +29,7 @@
diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
index 339cf8a334..924e844ec5 100644
--- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
@@ -78,18 +78,6 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data);
}
- [Fact]
- public async Task Should_Coerce_Get_Null_Double_String_To_UnsetValue()
- {
- var data = new Class1 { StringValue = null };
- var target = new BindingExpression(ExpressionObserver.Create(data, o => o.StringValue), typeof(double));
- var result = await target.Take(1);
-
- Assert.Equal(AvaloniaProperty.UnsetValue, result);
-
- GC.KeepAlive(data);
- }
-
[Fact]
public void Should_Convert_Set_String_To_Double()
{
@@ -249,19 +237,6 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data);
}
- [Fact]
- public void Should_Coerce_Setting_Null_Double_To_Default_Value()
- {
- var data = new Class1 { DoubleValue = 5.6 };
- var target = new BindingExpression(ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string));
-
- target.OnNext(null);
-
- Assert.Equal(0, data.DoubleValue);
-
- GC.KeepAlive(data);
- }
-
[Fact]
public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value()
{
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"),
+ };
+ }
+ }
+}
+
+
diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
index 3ba8e8354d..c312a71d44 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
@@ -648,16 +648,69 @@ namespace Avalonia.Markup.UnitTests.Data
};
}
+ [Fact]
+ public void Binding_Producing_Default_Value_Should_Result_In_Correct_Priority()
+ {
+ var defaultValue = StyledPropertyClass.NullableDoubleProperty.GetDefaultValue(typeof(StyledPropertyClass));
+
+ var vm = new NullableValuesViewModel() { NullableDouble = defaultValue };
+ var target = new StyledPropertyClass();
+
+ target.Bind(StyledPropertyClass.NullableDoubleProperty, new Binding(nameof(NullableValuesViewModel.NullableDouble)) { Source = vm });
+
+ Assert.Equal(BindingPriority.LocalValue, target.GetDiagnosticInternal(StyledPropertyClass.NullableDoubleProperty).Priority);
+ Assert.Equal(defaultValue, target.GetValue(StyledPropertyClass.NullableDoubleProperty));
+ }
+
+ [Fact]
+ public void Binding_Non_Nullable_ValueType_To_Null_Reverts_To_Default_Value()
+ {
+ var source = new NullableValuesViewModel { NullableDouble = 42 };
+ var target = new StyledPropertyClass();
+ var binding = new Binding(nameof(source.NullableDouble)) { Source = source };
+
+ target.Bind(StyledPropertyClass.DoubleValueProperty, binding);
+ Assert.Equal(42, target.DoubleValue);
+
+ source.NullableDouble = null;
+
+ Assert.Equal(12.3, target.DoubleValue);
+ }
+
+ [Fact]
+ public void Binding_Nullable_ValueType_To_Null_Sets_Value_To_Null()
+ {
+ var source = new NullableValuesViewModel { NullableDouble = 42 };
+ var target = new StyledPropertyClass();
+ var binding = new Binding(nameof(source.NullableDouble)) { Source = source };
+
+ target.Bind(StyledPropertyClass.NullableDoubleProperty, binding);
+ Assert.Equal(42, target.NullableDouble);
+
+ source.NullableDouble = null;
+
+ Assert.Null(target.NullableDouble);
+ }
+
private class StyledPropertyClass : AvaloniaObject
{
public static readonly StyledProperty DoubleValueProperty =
- AvaloniaProperty.Register(nameof(DoubleValue));
+ AvaloniaProperty.Register(nameof(DoubleValue), 12.3);
public double DoubleValue
{
get { return GetValue(DoubleValueProperty); }
set { SetValue(DoubleValueProperty, value); }
}
+
+ public static StyledProperty NullableDoubleProperty =
+ AvaloniaProperty.Register(nameof(NullableDoubleProperty), -1);
+
+ public double? NullableDouble
+ {
+ get => GetValue(NullableDoubleProperty);
+ set => SetValue(NullableDoubleProperty, value);
+ }
}
private class DirectPropertyClass : AvaloniaObject
@@ -676,6 +729,21 @@ namespace Avalonia.Markup.UnitTests.Data
}
}
+ private class NullableValuesViewModel : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ private double? _nullableDouble;
+ public double? NullableDouble
+ {
+ get => _nullableDouble; set
+ {
+ _nullableDouble = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NullableDouble)));
+ }
+ }
+ }
+
private class TestStackOverflowViewModel : INotifyPropertyChanged
{
public int SetterInvokedCount { get; private set; }