Browse Source

Merge branch 'master' into controls-nullability-cleanup

pull/10256/head
Max Katz 3 years ago
committed by GitHub
parent
commit
756d48ed0d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
  2. 62
      src/Avalonia.Controls/Automation/Peers/ProgressBarAutomationPeer.cs
  3. 29
      src/Avalonia.Controls/ItemsControl.cs
  4. 281
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  5. 2
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  6. 7
      src/Avalonia.Controls/ProgressBar.cs
  7. 2
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
  8. 1
      src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
  9. 25
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
  10. 330
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs
  11. 70
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

2
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)

62
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;
}
/// <summary>
/// Request to set the value that this UI element is representing
/// </summary>
/// <param name="val">Value to set the UI to, as an object</param>
/// <returns>true if the UI element was successfully set to the specified value</returns>
void IRangeValueProvider.SetValue(double val)
{
throw new InvalidOperationException("ProgressBar is ReadOnly, value can't be set.");
}
///<summary>Indicates that the value can only be read, not modified.
///returns True if the control is read-only</summary>
bool IRangeValueProvider.IsReadOnly
{
get
{
return true;
}
}
///<summary>Value of a Large Change</summary>
double IRangeValueProvider.LargeChange
{
get
{
return double.NaN;
}
}
///<summary>Value of a Small Change</summary>
double IRangeValueProvider.SmallChange
{
get
{
return double.NaN;
}
}
}
}

29
src/Avalonia.Controls/ItemsControl.cs

@ -89,10 +89,11 @@ namespace Avalonia.Controls
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
/// </summary>
[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<object>();
@ -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);
}
/// <summary>
@ -140,8 +141,8 @@ namespace Avalonia.Controls
/// </summary>
public ControlTheme? ItemContainerTheme
{
get { return GetValue(ItemContainerThemeProperty); }
set { SetValue(ItemContainerThemeProperty, value); }
get => GetValue(ItemContainerThemeProperty);
set => SetValue(ItemContainerThemeProperty, value);
}
/// <summary>
@ -158,8 +159,8 @@ namespace Avalonia.Controls
/// </summary>
public ITemplate<Panel> ItemsPanel
{
get { return GetValue(ItemsPanelProperty); }
set { SetValue(ItemsPanelProperty, value); }
get => GetValue(ItemsPanelProperty);
set => SetValue(ItemsPanelProperty, value);
}
/// <summary>
@ -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);
}
/// <summary>
@ -263,8 +264,8 @@ namespace Avalonia.Controls
/// </summary>
public bool AreHorizontalSnapPointsRegular
{
get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
get => GetValue(AreHorizontalSnapPointsRegularProperty);
set => SetValue(AreHorizontalSnapPointsRegularProperty, value);
}
/// <summary>
@ -272,8 +273,8 @@ namespace Avalonia.Controls
/// </summary>
public bool AreVerticalSnapPointsRegular
{
get { return GetValue(AreVerticalSnapPointsRegularProperty); }
set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
get => GetValue(AreVerticalSnapPointsRegularProperty);
set => SetValue(AreVerticalSnapPointsRegularProperty, value);
}
/// <summary>

281
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);
/// <summary>
/// Defines the <see cref="SelectedValue"/> property
/// </summary>
public static readonly StyledProperty<object?> SelectedValueProperty =
AvaloniaProperty.Register<SelectingItemsControl, object?>(nameof(SelectedValue),
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="SelectedValueBinding"/> property
/// </summary>
public static readonly StyledProperty<IBinding?> SelectedValueBindingProperty =
AvaloniaProperty.Register<SelectingItemsControl, IBinding?>(nameof(SelectedValueBinding));
/// <summary>
/// Defines the <see cref="SelectedItems"/> property.
/// </summary>
@ -127,6 +142,8 @@ namespace Avalonia.Controls.Primitives
private bool _ignoreContainerSelectionChanged;
private UpdateState? _updateState;
private bool _hasScrolledToSelectedItem;
private BindingHelper? _bindingHelper;
private bool _isSelectionChangeActive;
/// <summary>
/// Initializes static members of the <see cref="SelectingItemsControl"/> class.
@ -141,8 +158,8 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public event EventHandler<SelectionChangedEventArgs>? SelectionChanged
{
add { AddHandler(SelectionChangedEvent, value); }
remove { RemoveHandler(SelectionChangedEvent, value); }
add => AddHandler(SelectionChangedEvent, value);
remove => RemoveHandler(SelectionChangedEvent, value);
}
/// <summary>
@ -150,8 +167,8 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public bool AutoScrollToSelectedItem
{
get { return GetValue(AutoScrollToSelectedItemProperty); }
set { SetValue(AutoScrollToSelectedItemProperty, value); }
get => GetValue(AutoScrollToSelectedItemProperty);
set => SetValue(AutoScrollToSelectedItemProperty, value);
}
/// <summary>
@ -207,6 +224,28 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Gets the <see cref="IBinding"/> instance used to obtain the
/// <see cref="SelectedValue"/> property
/// </summary>
[AssignBinding]
[InheritDataTypeFromItems(nameof(Items))]
public IBinding? SelectedValueBinding
{
get => GetValue(SelectedValueBindingProperty);
set => SetValue(SelectedValueBindingProperty, value);
}
/// <summary>
/// Gets or sets the value of the selected item, obtained using
/// <see cref="SelectedValueBinding"/>
/// </summary>
public object? SelectedValue
{
get => GetValue(SelectedValueProperty);
set => SetValue(SelectedValueProperty, value);
}
/// <summary>
/// Gets or sets the selected items.
/// </summary>
@ -321,8 +360,8 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public bool IsTextSearchEnabled
{
get { return GetValue(IsTextSearchEnabledProperty); }
set { SetValue(IsTextSearchEnabledProperty, value); }
get => GetValue(IsTextSearchEnabledProperty);
set => SetValue(IsTextSearchEnabledProperty, value);
}
/// <summary>
@ -331,8 +370,8 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public bool WrapSelection
{
get { return GetValue(WrapSelectionProperty); }
set { SetValue(WrapSelectionProperty, value); }
get => GetValue(WrapSelectionProperty);
set => SetValue(WrapSelectionProperty, value);
}
/// <summary>
@ -344,8 +383,8 @@ namespace Avalonia.Controls.Primitives
/// </remarks>
protected SelectionMode SelectionMode
{
get { return GetValue(SelectionModeProperty); }
set { SetValue(SelectionModeProperty, value); }
get => GetValue(SelectionModeProperty);
set => SetValue(SelectionModeProperty, value);
}
/// <summary>
@ -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<IBinding>();
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;
}
}
}
/// <summary>
@ -822,6 +915,10 @@ namespace Avalonia.Controls.Primitives
new BindingValue<IList?>(SelectedItems));
_oldSelectedItems = SelectedItems;
}
else if (e.PropertyName == nameof(ISelectionModel.Source))
{
ClearValue(SelectedValueProperty);
}
}
/// <summary>
@ -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<int> _selectedIndex;
private Optional<object?> _selectedItem;
private Optional<object?> _selectedValue;
public int UpdateCount { get; set; }
public Optional<ISelectionModel> Selection { get; set; }
@ -1129,6 +1342,54 @@ namespace Avalonia.Controls.Primitives
_selectedIndex = default;
}
}
public Optional<object?> SelectedValue
{
get => _selectedValue;
set
{
_selectedValue = value;
}
}
}
/// <summary>
/// Helper class for evaluating a binding from an Item and IBinding instance
/// </summary>
private class BindingHelper : StyledElement
{
public BindingHelper(IBinding binding)
{
UpdateBinding(binding);
}
public static readonly StyledProperty<object> ValueProperty =
AvaloniaProperty.Register<BindingHelper, object>("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;
}
}
}

2
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);
/// <summary>

7
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

2
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;
}
}

1
src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml

@ -29,6 +29,7 @@
<Style Selector="ListBoxItem.handled" >
<Setter Property="Background" Value="#d9ffdc" />
<Setter Property="Foreground" Value="Black" />
</Style>
</UserControl.Styles>

25
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()
{

330
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<TestClass>
{
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<TestClass>
{
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<object>().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<object>().Single());
Assert.Empty(e.AddedItems);
called = true;
};
sic.SelectedValue = null;
Assert.True(called);
}
private static FuncControlTemplate Template()
{
return new FuncControlTemplate<SelectingItemsControl>((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<SelectingItemsControl>())
{
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<TestClass> GetItems()
{
return new List<TestClass>
{
new TestClass("Item1", "Alt1"),
new TestClass("Item2", "Alt2"),
new TestClass("Item3", "Alt3"),
new TestClass("Item4", "Alt4"),
new TestClass("Item5", "Alt5"),
};
}
}
}

70
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<double> DoubleValueProperty =
AvaloniaProperty.Register<StyledPropertyClass, double>(nameof(DoubleValue));
AvaloniaProperty.Register<StyledPropertyClass, double>(nameof(DoubleValue), 12.3);
public double DoubleValue
{
get { return GetValue(DoubleValueProperty); }
set { SetValue(DoubleValueProperty, value); }
}
public static StyledProperty<double?> NullableDoubleProperty =
AvaloniaProperty.Register<StyledPropertyClass, double?>(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; }

Loading…
Cancel
Save