Browse Source

Merge branch 'master' into fix_data_grid_style_duplicate_setter

pull/2753/head
Josua Jäger 7 years ago
committed by GitHub
parent
commit
d789acaf5f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      samples/ControlCatalog/MainView.xaml
  2. 4
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
  3. 33
      samples/ControlCatalog/Pages/TabStripPage.xaml
  4. 45
      samples/ControlCatalog/Pages/TabStripPage.xaml.cs
  5. 3
      samples/ControlCatalog/SideBar.xaml
  6. 5
      samples/RenderDemo/SideBar.xaml
  7. 2
      src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
  8. 26
      src/Avalonia.Controls/AutoCompleteBox.cs
  9. 3
      src/Avalonia.Controls/ComboBox.cs
  10. 39
      src/Avalonia.Controls/ContextMenu.cs
  11. 12
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  12. 17
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  13. 14
      src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
  14. 5
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  15. 15
      src/Avalonia.Controls/ItemsControl.cs
  16. 8
      src/Avalonia.Controls/ListBox.cs
  17. 46
      src/Avalonia.Controls/Menu.cs
  18. 5
      src/Avalonia.Controls/MenuBase.cs
  19. 2
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  20. 2
      src/Avalonia.Controls/Presenters/ItemContainerSync.cs
  21. 2
      src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
  22. 9
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  23. 15
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  24. 35
      src/Avalonia.Controls/Primitives/Popup.cs
  25. 54
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  26. 9
      src/Avalonia.Controls/Primitives/TabStrip.cs
  27. 35
      src/Avalonia.Controls/Templates/FuncMemberSelector.cs
  28. 18
      src/Avalonia.Controls/Templates/IMemberSelector.cs
  29. 105
      src/Avalonia.Controls/TreeView.cs
  30. 37
      src/Avalonia.Diagnostics/DevTools.xaml
  31. 8
      src/Avalonia.Diagnostics/DevTools.xaml.cs
  32. 2
      src/Avalonia.Diagnostics/ViewLocator.cs
  33. 65
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  34. 13
      src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs
  35. 16
      src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs
  36. 7
      src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs
  37. 16
      src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
  38. 5
      src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs
  39. 3
      src/Avalonia.Diagnostics/Views/TreePage.xaml.cs
  40. 1
      src/Avalonia.Themes.Default/AutoCompleteBox.xaml
  41. 3
      src/Avalonia.Themes.Default/Carousel.xaml
  42. 1
      src/Avalonia.Themes.Default/ComboBox.xaml
  43. 2
      src/Avalonia.Themes.Default/DataValidationErrors.xaml
  44. 5
      src/Avalonia.Themes.Default/ItemsControl.xaml
  45. 3
      src/Avalonia.Themes.Default/ListBox.xaml
  46. 6
      src/Avalonia.Themes.Default/MenuItem.xaml
  47. 3
      src/Avalonia.Themes.Default/TabControl.xaml
  48. 3
      src/Avalonia.Themes.Default/TabStrip.xaml
  49. 3
      src/Avalonia.Themes.Default/TreeView.xaml
  50. 3
      src/Avalonia.Themes.Default/TreeViewItem.xaml
  51. 5
      src/Avalonia.Visuals/Rendering/RenderLayers.cs
  52. 2
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  53. 24
      src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs
  54. 48
      src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
  55. 2
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  56. 78
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs
  57. 54
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  58. 6
      tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs
  59. 2
      tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs
  60. 22
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  61. 40
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs
  62. 48
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  63. 115
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  64. 55
      tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs
  65. 23
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  66. 125
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  67. 55
      tests/Avalonia.LeakTests/MemberSelectorTests.cs
  68. 159
      tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs
  69. 1
      tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs

1
samples/ControlCatalog/MainView.xaml

@ -36,6 +36,7 @@
<TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>
<TabItem Header="Slider"><pages:SliderPage/></TabItem>
<TabItem Header="TabControl"><pages:TabControlPage/></TabItem>
<TabItem Header="TabStrip"><pages:TabStripPage/></TabItem>
<TabItem Header="TextBox"><pages:TextBoxPage/></TabItem>
<TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem>
<TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>

4
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml

@ -37,10 +37,6 @@
<StackPanel Orientation="Vertical">
<TextBlock Text="ValueMemeberSelector"/>
<AutoCompleteBox Width="200"
Margin="0,0,0,8"
ValueMemberSelector="Capital"/>
<TextBlock Text="ValueMemberBinding"/>
<AutoCompleteBox Width="200"
Margin="0,0,0,8"

33
samples/ControlCatalog/Pages/TabStripPage.xaml

@ -0,0 +1,33 @@
<UserControl xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.TabStripPage"
xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">TabStrip</TextBlock>
<TextBlock Classes="h2">A control which displays a selectable strip of tabs</TextBlock>
<Separator Margin="0 16"/>
<TextBlock Classes="h1">Defined in XAML</TextBlock>
<TabStrip>
<TabStripItem>Item 1</TabStripItem>
<TabStripItem>Item 2</TabStripItem>
<TabStripItem IsEnabled="False">Disabled</TabStripItem>
</TabStrip>
<Separator Margin="0 16"/>
<TextBlock Classes="h1">Dynamically generated</TextBlock>
<TabStrip Items="{Binding}">
<TabStrip.Styles>
<Style Selector="TabStripItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
</Style>
</TabStrip.Styles>
<TabStrip.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}"/>
</DataTemplate>
</TabStrip.ItemTemplate>
</TabStrip>
</StackPanel>
</UserControl>

45
samples/ControlCatalog/Pages/TabStripPage.xaml.cs

@ -0,0 +1,45 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace ControlCatalog.Pages
{
public class TabStripPage : UserControl
{
public TabStripPage()
{
InitializeComponent();
DataContext = new[]
{
new TabStripItemViewModel
{
Header = "Item 1",
},
new TabStripItemViewModel
{
Header = "Item 2",
},
new TabStripItemViewModel
{
Header = "Disabled",
IsEnabled = false,
},
};
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private class TabStripItemViewModel
{
public string Header { get; set; }
public bool IsEnabled { get; set; } = true;
}
}
}

3
samples/ControlCatalog/SideBar.xaml

@ -29,8 +29,7 @@
Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
MemberSelector="{TemplateBinding MemberSelector}">
ItemTemplate="{TemplateBinding ItemTemplate}">
</ItemsPresenter>
</ScrollViewer>
<ContentPresenter

5
samples/RenderDemo/SideBar.xaml

@ -20,8 +20,7 @@
Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
MemberSelector="{TemplateBinding MemberSelector}">
ItemTemplate="{TemplateBinding ItemTemplate}">
</ItemsPresenter>
</ScrollViewer>
<ContentPresenter
@ -63,4 +62,4 @@
<Style Selector="TabControl.sidebar > TabItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush2}"/>
</Style>
</Styles>
</Styles>

2
src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs

@ -31,7 +31,7 @@ namespace Avalonia.Data.Converters
{
if (value == null)
{
return AvaloniaProperty.UnsetValue;
return targetType.IsValueType ? AvaloniaProperty.UnsetValue : null;
}
if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1)

26
src/Avalonia.Controls/AutoCompleteBox.cs

@ -345,7 +345,6 @@ namespace Avalonia.Controls
/// </summary>
private IDisposable _collectionChangeSubscription;
private IMemberSelector _valueMemberSelector;
private Func<string, CancellationToken, Task<IEnumerable<object>>> _asyncPopulator;
private CancellationTokenSource _populationCancellationTokenSource;
@ -541,12 +540,6 @@ namespace Avalonia.Controls
o => o.Items,
(o, v) => o.Items = v);
public static readonly DirectProperty<AutoCompleteBox, IMemberSelector> ValueMemberSelectorProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, IMemberSelector>(
nameof(ValueMemberSelector),
o => o.ValueMemberSelector,
(o, v) => o.ValueMemberSelector = v);
public static readonly DirectProperty<AutoCompleteBox, Func<string, CancellationToken, Task<IEnumerable<object>>>> AsyncPopulatorProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, Func<string, CancellationToken, Task<IEnumerable<object>>>>(
nameof(AsyncPopulator),
@ -958,20 +951,6 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Gets or sets the MemberSelector that is used to get values for
/// display in the text portion of the
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
/// </summary>
/// <value>The MemberSelector that is used to get values for display in
/// the text portion of the
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.</value>
public IMemberSelector ValueMemberSelector
{
get { return _valueMemberSelector; }
set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); }
}
/// <summary>
/// Gets or sets the selected item in the drop-down.
/// </summary>
@ -1841,11 +1820,6 @@ namespace Avalonia.Controls
return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty;
}
if (_valueMemberSelector != null)
{
value = _valueMemberSelector.Select(value);
}
return value == null ? String.Empty : value.ToString();
}

3
src/Avalonia.Controls/ComboBox.cs

@ -333,8 +333,7 @@ namespace Avalonia.Controls
}
else
{
var selector = MemberSelector;
SelectionBoxItem = selector != null ? selector.Select(item) : item;
SelectionBoxItem = item;
}
}

39
src/Avalonia.Controls/ContextMenu.cs

@ -1,12 +1,12 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
namespace Avalonia.Controls
@ -90,9 +90,14 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param>
public void Open(Control control)
{
if (IsOpen)
{
return;
}
if (_popup == null)
{
_popup = new Popup()
_popup = new Popup
{
PlacementMode = PlacementMode.Pointer,
PlacementTarget = control,
@ -107,7 +112,14 @@ namespace Avalonia.Controls
((ISetLogicalParent)_popup).SetParent(control);
_popup.Child = this;
_popup.IsOpen = true;
IsOpen = true;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuOpenedEvent,
Source = this,
});
}
/// <summary>
@ -115,13 +127,15 @@ namespace Avalonia.Controls
/// </summary>
public override void Close()
{
if (!IsOpen)
{
return;
}
if (_popup != null && _popup.IsVisible)
{
_popup.IsOpen = false;
}
SelectedIndex = -1;
IsOpen = false;
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
@ -129,6 +143,18 @@ namespace Avalonia.Controls
return new MenuItemContainerGenerator(this);
}
private void CloseCore()
{
SelectedIndex = -1;
IsOpen = false;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuClosedEvent,
Source = this,
});
}
private void PopupOpened(object sender, EventArgs e)
{
Focus();
@ -145,8 +171,7 @@ namespace Avalonia.Controls
i.IsSubMenuOpen = false;
}
contextMenu.IsOpen = false;
contextMenu.SelectedIndex = -1;
contextMenu.CloseCore();
}
}

12
src/Avalonia.Controls/Generators/IItemContainerGenerator.cs

@ -49,12 +49,8 @@ namespace Avalonia.Controls.Generators
/// The index of the item of data in the control's items.
/// </param>
/// <param name="item">The item.</param>
/// <param name="selector">An optional member selector.</param>
/// <returns>The created controls.</returns>
ItemContainerInfo Materialize(
int index,
object item,
IMemberSelector selector);
ItemContainerInfo Materialize(int index, object item);
/// <summary>
/// Removes a set of created containers.
@ -84,11 +80,7 @@ namespace Avalonia.Controls.Generators
/// <returns>The removed containers.</returns>
IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count);
bool TryRecycle(
int oldIndex,
int newIndex,
object item,
IMemberSelector selector);
bool TryRecycle(int oldIndex, int newIndex, object item);
/// <summary>
/// Clears all created containers and returns the removed controls.

17
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@ -54,13 +54,9 @@ namespace Avalonia.Controls.Generators
public virtual Type ContainerType => null;
/// <inheritdoc/>
public ItemContainerInfo Materialize(
int index,
object item,
IMemberSelector selector)
public ItemContainerInfo Materialize(int index, object item)
{
var i = selector != null ? selector.Select(item) : item;
var container = new ItemContainerInfo(CreateContainer(i), item, index);
var container = new ItemContainerInfo(CreateContainer(item), item, index);
_containers.Add(container.Index, container);
Materialized?.Invoke(this, new ItemContainerEventArgs(container));
@ -138,14 +134,7 @@ namespace Avalonia.Controls.Generators
}
/// <inheritdoc/>
public virtual bool TryRecycle(
int oldIndex,
int newIndex,
object item,
IMemberSelector selector)
{
return false;
}
public virtual bool TryRecycle(int oldIndex, int newIndex, object item) => false;
/// <inheritdoc/>
public virtual IEnumerable<ItemContainerInfo> Clear()

14
src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs

@ -79,11 +79,7 @@ namespace Avalonia.Controls.Generators
}
/// <inheritdoc/>
public override bool TryRecycle(
int oldIndex,
int newIndex,
object item,
IMemberSelector selector)
public override bool TryRecycle(int oldIndex, int newIndex, object item)
{
var container = ContainerFromIndex(oldIndex);
@ -92,16 +88,14 @@ namespace Avalonia.Controls.Generators
throw new IndexOutOfRangeException("Could not recycle container: not materialized.");
}
var i = selector != null ? selector.Select(item) : item;
container.SetValue(ContentProperty, i);
container.SetValue(ContentProperty, item);
if (!(item is IControl))
{
container.DataContext = i;
container.DataContext = item;
}
var info = MoveContainer(oldIndex, newIndex, i);
var info = MoveContainer(oldIndex, newIndex, item);
RaiseRecycled(new ItemContainerEventArgs(info));
return true;

5
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@ -117,10 +117,7 @@ namespace Avalonia.Controls.Generators
return base.RemoveRange(startingIndex, count);
}
public override bool TryRecycle(int oldIndex, int newIndex, object item, IMemberSelector selector)
{
return false;
}
public override bool TryRecycle(int oldIndex, int newIndex, object item) => false;
class WrapperTreeDataTemplate : ITreeDataTemplate
{

15
src/Avalonia.Controls/ItemsControl.cs

@ -54,12 +54,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
AvaloniaProperty.Register<ItemsControl, IDataTemplate>(nameof(ItemTemplate));
/// <summary>
/// Defines the <see cref="MemberSelector"/> property.
/// </summary>
public static readonly StyledProperty<IMemberSelector> MemberSelectorProperty =
AvaloniaProperty.Register<ItemsControl, IMemberSelector>(nameof(MemberSelector));
private IEnumerable _items = new AvaloniaList<object>();
private int _itemCount;
private IItemContainerGenerator _itemContainerGenerator;
@ -144,15 +138,6 @@ namespace Avalonia.Controls
set { SetValue(ItemTemplateProperty, value); }
}
/// <summary>
/// Selects a member from <see cref="Items"/> to use as the list item.
/// </summary>
public IMemberSelector MemberSelector
{
get { return GetValue(MemberSelectorProperty); }
set { SetValue(MemberSelectorProperty, value); }
}
/// <summary>
/// Gets the items presenter control.
/// </summary>

8
src/Avalonia.Controls/ListBox.cs

@ -68,7 +68,13 @@ namespace Avalonia.Controls
/// <inheritdoc/>
public new IList SelectedItems => base.SelectedItems;
/// <inheritdoc/>
/// <summary>
/// Gets or sets the selection mode.
/// </summary>
/// <remarks>
/// Note that the selection mode only applies to selections made via user interaction.
/// Multiple selections can be made programatically regardless of the value of this property.
/// </remarks>
public new SelectionMode SelectionMode
{
get { return base.SelectionMode; }

46
src/Avalonia.Controls/Menu.cs

@ -40,37 +40,41 @@ namespace Avalonia.Controls
/// <inheritdoc/>
public override void Close()
{
if (IsOpen)
if (!IsOpen)
{
foreach (var i in ((IMenu)this).SubItems)
{
i.Close();
}
IsOpen = false;
SelectedIndex = -1;
return;
}
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuClosedEvent,
Source = this,
});
foreach (var i in ((IMenu)this).SubItems)
{
i.Close();
}
IsOpen = false;
SelectedIndex = -1;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuClosedEvent,
Source = this,
});
}
/// <inheritdoc/>
public override void Open()
{
if (!IsOpen)
if (IsOpen)
{
IsOpen = true;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuOpenedEvent,
Source = this,
});
return;
}
IsOpen = true;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuOpenedEvent,
Source = this,
});
}
/// <inheritdoc/>

5
src/Avalonia.Controls/MenuBase.cs

@ -7,7 +7,6 @@ using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
@ -31,13 +30,13 @@ namespace Avalonia.Controls
/// Defines the <see cref="MenuOpened"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> MenuOpenedEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuOpened), RoutingStrategies.Bubble);
RoutedEvent.Register<MenuBase, RoutedEventArgs>(nameof(MenuOpened), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="MenuClosed"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> MenuClosedEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuClosed), RoutingStrategies.Bubble);
RoutedEvent.Register<MenuBase, RoutedEventArgs>(nameof(MenuClosed), RoutingStrategies.Bubble);
private bool _isOpen;

2
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@ -213,7 +213,7 @@ namespace Avalonia.Controls.Presenters
if (container == null && IsVirtualized)
{
var item = Items.Cast<object>().ElementAt(index);
var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector);
var materialized = ItemContainerGenerator.Materialize(index, item);
Panel.Children.Add(materialized.ContainerControl);
container = materialized.ContainerControl;
}

2
src/Avalonia.Controls/Presenters/ItemContainerSync.cs

@ -88,7 +88,7 @@ namespace Avalonia.Controls.Presenters
foreach (var item in items)
{
var i = generator.Materialize(index++, item, owner.MemberSelector);
var i = generator.Materialize(index++, item);
if (i.ContainerControl != null)
{

2
src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs

@ -90,7 +90,7 @@ namespace Avalonia.Controls.Presenters
foreach (var item in items)
{
var i = generator.Materialize(index++, item, Owner.MemberSelector);
var i = generator.Materialize(index++, item);
if (i.ContainerControl != null)
{

9
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -314,7 +314,6 @@ namespace Avalonia.Controls.Presenters
if (!panel.IsFull && Items != null && panel.IsAttachedToVisualTree)
{
var memberSelector = Owner.MemberSelector;
var index = NextIndex;
var step = 1;
@ -337,7 +336,7 @@ namespace Avalonia.Controls.Presenters
}
}
var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector);
var materialized = generator.Materialize(index, Items.ElementAt(index));
if (step == 1)
{
@ -383,7 +382,6 @@ namespace Avalonia.Controls.Presenters
{
var panel = VirtualizingPanel;
var generator = Owner.ItemContainerGenerator;
var selector = Owner.MemberSelector;
var containers = generator.Containers.ToList();
var itemIndex = FirstIndex;
@ -393,7 +391,7 @@ namespace Avalonia.Controls.Presenters
if (!object.Equals(container.Item, item))
{
if (!generator.TryRecycle(itemIndex, itemIndex, item, selector))
if (!generator.TryRecycle(itemIndex, itemIndex, item))
{
throw new NotImplementedException();
}
@ -420,7 +418,6 @@ namespace Avalonia.Controls.Presenters
{
var panel = VirtualizingPanel;
var generator = Owner.ItemContainerGenerator;
var selector = Owner.MemberSelector;
//validate delta it should never overflow last index or generate index < 0
delta = MathUtilities.Clamp(delta, -FirstIndex, ItemCount - FirstIndex - panel.Children.Count);
@ -437,7 +434,7 @@ namespace Avalonia.Controls.Presenters
var item = Items.ElementAt(newItemIndex);
if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector))
if (!generator.TryRecycle(oldItemIndex, newItemIndex, item))
{
throw new NotImplementedException();
}

15
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@ -35,12 +35,6 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
ItemsControl.ItemTemplateProperty.AddOwner<ItemsPresenterBase>();
/// <summary>
/// Defines the <see cref="MemberSelector"/> property.
/// </summary>
public static readonly StyledProperty<IMemberSelector> MemberSelectorProperty =
ItemsControl.MemberSelectorProperty.AddOwner<ItemsPresenterBase>();
private IEnumerable _items;
private IDisposable _itemsSubscription;
private bool _createdPanel;
@ -127,15 +121,6 @@ namespace Avalonia.Controls.Presenters
set { SetValue(ItemTemplateProperty, value); }
}
/// <summary>
/// Selects a member from <see cref="Items"/> to use as the list item.
/// </summary>
public IMemberSelector MemberSelector
{
get { return GetValue(MemberSelectorProperty); }
set { SetValue(MemberSelectorProperty, value); }
}
/// <summary>
/// Gets the panel used to display the items.
/// </summary>

35
src/Avalonia.Controls/Primitives/Popup.cs

@ -6,7 +6,6 @@ using System.Linq;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.VisualTree;
@ -270,9 +269,10 @@ namespace Avalonia.Controls.Primitives
_popupRoot.SnapInsideScreenEdges();
}
_ignoreIsOpenChanged = true;
IsOpen = true;
_ignoreIsOpenChanged = false;
using (BeginIgnoringIsOpen())
{
IsOpen = true;
}
Opened?.Invoke(this, EventArgs.Empty);
}
@ -305,7 +305,11 @@ namespace Avalonia.Controls.Primitives
_popupRoot.Hide();
}
IsOpen = false;
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
Closed?.Invoke(this, EventArgs.Empty);
}
@ -467,5 +471,26 @@ namespace Avalonia.Controls.Primitives
Close();
}
}
private IgnoreIsOpenScope BeginIgnoringIsOpen()
{
return new IgnoreIsOpenScope(this);
}
private readonly struct IgnoreIsOpenScope : IDisposable
{
private readonly Popup _owner;
public IgnoreIsOpenScope(Popup owner)
{
_owner = owner;
_owner._ignoreIsOpenChanged = true;
}
public void Dispose()
{
_owner._ignoreIsOpenChanged = false;
}
}
}
}

54
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -222,6 +222,10 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Gets or sets the selection mode.
/// </summary>
/// <remarks>
/// Note that the selection mode only applies to selections made via user interaction.
/// Multiple selections can be made programatically regardless of the value of this property.
/// </remarks>
protected SelectionMode SelectionMode
{
get { return GetValue(SelectionModeProperty); }
@ -338,24 +342,36 @@ namespace Avalonia.Controls.Primitives
{
base.OnContainersMaterialized(e);
var selectedIndex = SelectedIndex;
var selectedContainer = e.Containers
.FirstOrDefault(x => (x.ContainerControl as ISelectable)?.IsSelected == true);
var resetSelectedItems = false;
if (selectedContainer != null)
foreach (var container in e.Containers)
{
SelectedIndex = selectedContainer.Index;
}
else if (selectedIndex >= e.StartingIndex &&
selectedIndex < e.StartingIndex + e.Containers.Count)
{
var container = e.Containers[selectedIndex - e.StartingIndex];
if ((container.ContainerControl as ISelectable)?.IsSelected == true)
{
if (SelectedIndex == -1)
{
SelectedIndex = container.Index;
}
else
{
if (_selection.Add(container.Index))
{
resetSelectedItems = true;
}
}
if (container.ContainerControl != null)
MarkContainerSelected(container.ContainerControl, true);
}
else if (_selection.Contains(container.Index))
{
MarkContainerSelected(container.ContainerControl, true);
}
}
if (resetSelectedItems)
{
ResetSelectedItems();
}
}
/// <inheritdoc/>
@ -469,11 +485,6 @@ namespace Avalonia.Controls.Primitives
/// </summary>
protected void SelectAll()
{
if ((SelectionMode & (SelectionMode.Multiple | SelectionMode.Toggle)) == 0)
{
throw new NotSupportedException("Multiple selection is not enabled on this control.");
}
UpdateSelectedItems(() =>
{
_selection.Clear();
@ -523,7 +534,14 @@ namespace Avalonia.Controls.Primitives
var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
var range = multi && rangeModifier;
if (range)
if (rightButton)
{
if (!_selection.Contains(index))
{
UpdateSelectedItem(index);
}
}
else if (range)
{
UpdateSelectedItems(() =>
{
@ -582,7 +600,7 @@ namespace Avalonia.Controls.Primitives
}
else
{
UpdateSelectedItem(index, !(rightButton && _selection.Contains(index)));
UpdateSelectedItem(index);
}
if (Presenter?.Panel != null)

9
src/Avalonia.Controls/Primitives/TabStrip.cs

@ -12,11 +12,8 @@ namespace Avalonia.Controls.Primitives
private static readonly FuncTemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new WrapPanel { Orientation = Orientation.Horizontal });
private static IMemberSelector s_MemberSelector = new FuncMemberSelector<object, object>(SelectHeader);
static TabStrip()
{
MemberSelectorProperty.OverrideDefaultValue<TabStrip>(s_MemberSelector);
SelectionModeProperty.OverrideDefaultValue<TabStrip>(SelectionMode.AlwaysSelected);
FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false);
ItemsPanelProperty.OverrideDefaultValue<TabStrip>(DefaultPanel);
@ -51,11 +48,5 @@ namespace Avalonia.Controls.Primitives
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
private static object SelectHeader(object o)
{
var headered = o as IHeadered;
return (headered != null) ? (headered.Header ?? string.Empty) : o;
}
}
}

35
src/Avalonia.Controls/Templates/FuncMemberSelector.cs

@ -1,35 +0,0 @@
// 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;
namespace Avalonia.Controls.Templates
{
/// <summary>
/// Selects a member of an object using a <see cref="Func{TObject, TMember}"/>.
/// </summary>
public class FuncMemberSelector<TObject, TMember> : IMemberSelector
{
private readonly Func<TObject, TMember> _selector;
/// <summary>
/// Initializes a new instance of the <see cref="FuncMemberSelector{TObject, TMember}"/>
/// class.
/// </summary>
/// <param name="selector">The selector.</param>
public FuncMemberSelector(Func<TObject, TMember> selector)
{
this._selector = selector;
}
/// <summary>
/// Selects a member of an object.
/// </summary>
/// <param name="o">The object.</param>
/// <returns>The selected member.</returns>
public object Select(object o)
{
return (o is TObject) ? _selector((TObject)o) : default(TMember);
}
}
}

18
src/Avalonia.Controls/Templates/IMemberSelector.cs

@ -1,18 +0,0 @@
// 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.
namespace Avalonia.Controls.Templates
{
/// <summary>
/// Selects a member of an object.
/// </summary>
public interface IMemberSelector
{
/// <summary>
/// Selects a member of an object.
/// </summary>
/// <param name="o">The object.</param>
/// <returns>The selected member.</returns>
object Select(object o);
}
}

105
src/Avalonia.Controls/TreeView.cs

@ -105,32 +105,21 @@ namespace Avalonia.Controls
get => _selectedItem;
set
{
SetAndRaise(SelectedItemProperty, ref _selectedItem,
(object val, ref object backing, Action<Action> notifyWrapper) =>
{
var old = backing;
backing = val;
notifyWrapper(() =>
RaisePropertyChanged(
SelectedItemProperty,
old,
val));
SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
if (val != null)
{
if (SelectedItems.Count != 1 || SelectedItems[0] != val)
{
_syncingSelectedItems = true;
SelectSingleItem(val);
_syncingSelectedItems = false;
}
}
else if (SelectedItems.Count > 0)
{
SelectedItems.Clear();
}
}, value);
if (value != null)
{
if (SelectedItems.Count != 1 || SelectedItems[0] != value)
{
_syncingSelectedItems = true;
SelectSingleItem(value);
_syncingSelectedItems = false;
}
}
else if (SelectedItems.Count > 0)
{
SelectedItems.Clear();
}
}
}
@ -164,6 +153,48 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Expands the specified <see cref="TreeViewItem"/> all descendent <see cref="TreeViewItem"/>s.
/// </summary>
/// <param name="item">The item to expand.</param>
public void ExpandSubTree(TreeViewItem item)
{
item.IsExpanded = true;
var panel = item.Presenter.Panel;
if (panel != null)
{
foreach (var child in panel.Children)
{
if (child is TreeViewItem treeViewItem)
{
ExpandSubTree(treeViewItem);
}
}
}
}
/// <summary>
/// Selects all items in the <see cref="TreeView"/>.
/// </summary>
/// <remarks>
/// Note that this method only selects nodes currently visible due to their parent nodes
/// being expanded: it does not expand nodes.
/// </remarks>
public void SelectAll()
{
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
}
/// <summary>
/// Deselects all items in the <see cref="TreeView"/>.
/// </summary>
public void UnselectAll()
{
SelectedItems.Clear();
}
/// <summary>
/// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary>
@ -409,7 +440,7 @@ namespace Avalonia.Controls
if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll))
{
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
SelectAll();
e.Handled = true;
}
}
@ -479,7 +510,8 @@ namespace Avalonia.Controls
e.Source,
true,
(e.InputModifiers & InputModifiers.Shift) != 0,
(e.InputModifiers & InputModifiers.Control) != 0);
(e.InputModifiers & InputModifiers.Control) != 0,
e.MouseButton == MouseButton.Right);
}
}
@ -490,11 +522,13 @@ namespace Avalonia.Controls
/// <param name="select">Whether the item should be selected or unselected.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
protected void UpdateSelectionFromContainer(
IControl container,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
bool toggleModifier = false,
bool rightButton = false)
{
var item = ItemContainerGenerator.Index.ItemFromContainer(container);
@ -515,7 +549,14 @@ namespace Avalonia.Controls
var multi = (mode & SelectionMode.Multiple) != 0;
var range = multi && selectedContainer != null && rangeModifier;
if (!toggle && !range)
if (rightButton)
{
if (!SelectedItems.Contains(item))
{
SelectSingleItem(item);
}
}
else if (!toggle && !range)
{
SelectSingleItem(item);
}
@ -684,6 +725,7 @@ namespace Avalonia.Controls
/// <param name="select">Whether the container should be selected or unselected.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
/// <returns>
/// True if the event originated from a container that belongs to the control; otherwise
/// false.
@ -692,13 +734,14 @@ namespace Avalonia.Controls
IInteractive eventSource,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
bool toggleModifier = false,
bool rightButton = false)
{
var container = GetContainerFromEventSource(eventSource);
if (container != null)
{
UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier);
UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier, rightButton);
return true;
}

37
src/Avalonia.Diagnostics/DevTools.xaml

@ -1,23 +1,24 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia.Diagnostics.DevTools">
<Grid RowDefinitions="Auto,*,Auto">
<TabStrip SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
<TabStripItem Content="Logical Tree"/>
<TabStripItem Content="Visual Tree"/>
<TabStripItem Content="Events"/>
</TabStrip>
<Grid RowDefinitions="*,Auto" Margin="4">
<ContentControl Content="{Binding Content}" Grid.Row="1"/>
<StackPanel Spacing="4" Orientation="Horizontal" Grid.Row="2">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8"/>
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}"/>
<Separator Width="8"/>
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}"/>
</StackPanel>
</Grid>
<TabControl Grid.Row="0" Items="{Binding Tools}" SelectedItem="{Binding SelectedTool}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
<StackPanel Grid.Row="1" Spacing="4" Orientation="Horizontal">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8" />
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}" />
<Separator Width="8" />
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}" />
</StackPanel>
</Grid>
</UserControl>

8
src/Avalonia.Diagnostics/DevTools.xaml.cs

@ -1,10 +1,13 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Input.Raw;
@ -82,7 +85,8 @@ namespace Avalonia.Diagnostics
DataTemplates =
{
new ViewLocator<ViewModelBase>(),
}
},
Title = "Avalonia DevTools"
};
devToolsWindow.Closed += devTools.DevToolsClosed;

2
src/Avalonia.Diagnostics/ViewLocator.cs

@ -31,4 +31,4 @@ namespace Avalonia.Diagnostics
return data is TViewModel;
}
}
}
}

65
src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs

@ -2,7 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Input;
@ -10,21 +12,23 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class DevToolsViewModel : ViewModelBase
{
private ViewModelBase _content;
private int _selectedTab;
private TreePageViewModel _logicalTree;
private TreePageViewModel _visualTree;
private EventsViewModel _eventsView;
private IDevToolViewModel _selectedTool;
private string _focusedControl;
private string _pointerOverElement;
public DevToolsViewModel(IControl root)
{
_logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
_eventsView = new EventsViewModel(root);
Tools = new ObservableCollection<IDevToolViewModel>
{
new TreePageViewModel(LogicalTreeNode.Create(root), "Logical Tree"),
new TreePageViewModel(VisualTreeNode.Create(root), "Visual Tree"),
new EventsViewModel(root)
};
SelectedTool = Tools.First();
UpdateFocusedControl();
KeyboardDevice.Instance.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
@ -33,58 +37,33 @@ namespace Avalonia.Diagnostics.ViewModels
}
};
SelectedTab = 0;
root.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x?.GetType().Name);
}
public ViewModelBase Content
public IDevToolViewModel SelectedTool
{
get { return _content; }
private set { RaiseAndSetIfChanged(ref _content, value); }
get => _selectedTool;
set => RaiseAndSetIfChanged(ref _selectedTool, value);
}
public int SelectedTab
{
get { return _selectedTab; }
set
{
_selectedTab = value;
switch (value)
{
case 0:
Content = _logicalTree;
break;
case 1:
Content = _visualTree;
break;
case 2:
Content = _eventsView;
break;
}
RaisePropertyChanged();
}
}
public ObservableCollection<IDevToolViewModel> Tools { get; }
public string FocusedControl
{
get { return _focusedControl; }
private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
get => _focusedControl;
private set => RaiseAndSetIfChanged(ref _focusedControl, value);
}
public string PointerOverElement
{
get { return _pointerOverElement; }
private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); }
get => _pointerOverElement;
private set => RaiseAndSetIfChanged(ref _pointerOverElement, value);
}
public void SelectControl(IControl control)
{
var tree = Content as TreePageViewModel;
if (tree != null)
if (SelectedTool is TreePageViewModel tree)
{
tree.SelectControl(control);
}

13
src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs

@ -5,8 +5,6 @@ using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Windows.Input;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Interactivity;
@ -14,21 +12,24 @@ using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventsViewModel : ViewModelBase
internal class EventsViewModel : ViewModelBase, IDevToolViewModel
{
private readonly IControl _root;
private FiredEvent _selectedEvent;
public EventsViewModel(IControl root)
{
this._root = root;
this.Nodes = RoutedEventRegistry.Instance.GetAllRegistered()
_root = root;
Nodes = RoutedEventRegistry.Instance.GetAllRegistered()
.GroupBy(e => e.OwnerType)
.OrderBy(e => e.Key.Name)
.Select(g => new EventOwnerTreeNode(g.Key, g, this))
.ToArray();
}
public string Name => "Events";
public EventTreeNodeBase[] Nodes { get; }
public ObservableCollection<FiredEvent> RecordedEvents { get; } = new ObservableCollection<FiredEvent>();
@ -49,7 +50,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? Brushes.LightGreen : Brushes.Transparent;
return (bool)value ? Brushes.Green : Brushes.Transparent;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

16
src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs

@ -0,0 +1,16 @@
// 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.
namespace Avalonia.Diagnostics.ViewModels
{
/// <summary>
/// View model interface for tool showing up in DevTools
/// </summary>
public interface IDevToolViewModel
{
/// <summary>
/// Name of a tool.
/// </summary>
string Name { get; }
}
}

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

@ -6,16 +6,19 @@ using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
internal class TreePageViewModel : ViewModelBase
internal class TreePageViewModel : ViewModelBase, IDevToolViewModel
{
private TreeNode _selected;
private ControlDetailsViewModel _details;
public TreePageViewModel(TreeNode[] nodes)
public TreePageViewModel(TreeNode[] nodes, string name)
{
Nodes = nodes;
Name = name;
}
public string Name { get; }
public TreeNode[] Nodes { get; protected set; }
public TreeNode SelectedNode

16
src/Avalonia.Diagnostics/Views/ControlDetailsView.cs

@ -7,7 +7,6 @@ using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Media;
using Avalonia.Styling;
namespace Avalonia.Diagnostics.Views
{
@ -42,16 +41,6 @@ namespace Avalonia.Diagnostics.Views
{
Content = _grid = new SimpleGrid
{
Styles =
{
new Style(x => x.Is<Control>())
{
Setters = new[]
{
new Setter(MarginProperty, new Thickness(2)),
}
},
},
[GridRepeater.TemplateProperty] = pt,
}
};
@ -61,8 +50,11 @@ namespace Avalonia.Diagnostics.Views
{
var property = (PropertyDetails)i;
var margin = new Thickness(2);
yield return new TextBlock
{
Margin = margin,
Text = property.Name,
TextWrapping = TextWrapping.NoWrap,
[!ToolTip.TipProperty] = property.GetObservable<string>(nameof(property.Diagnostic)).ToBinding(),
@ -70,6 +62,7 @@ namespace Avalonia.Diagnostics.Views
yield return new TextBlock
{
Margin = margin,
TextWrapping = TextWrapping.NoWrap,
[!TextBlock.TextProperty] = property.GetObservable<object>(nameof(property.Value))
.Select(v => v?.ToString())
@ -78,6 +71,7 @@ namespace Avalonia.Diagnostics.Views
yield return new TextBlock
{
Margin = margin,
TextWrapping = TextWrapping.NoWrap,
[!TextBlock.TextProperty] = property.GetObservable<string>((nameof(property.Priority))).ToBinding(),
};

5
src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs → src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs

@ -1,4 +1,7 @@
using System;
// 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;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Reflection;

3
src/Avalonia.Diagnostics/Views/TreePage.xaml.cs

@ -1,3 +1,6 @@
// 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 Avalonia.Controls;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives;

1
src/Avalonia.Themes.Default/AutoCompleteBox.xaml

@ -27,7 +27,6 @@
Background="{TemplateBinding Background}"
Foreground="{TemplateBinding Foreground}"
ItemTemplate="{TemplateBinding ItemTemplate}"
MemberSelector="{TemplateBinding ValueMemberSelector}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
</Border>

3
src/Avalonia.Themes.Default/Carousel.xaml

@ -8,10 +8,9 @@
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{TemplateBinding Padding}"
MemberSelector="{TemplateBinding MemberSelector}"
SelectedIndex="{TemplateBinding SelectedIndex}"
PageTransition="{TemplateBinding PageTransition}"/>
</Border>
</ControlTemplate>
</Setter>
</Style>
</Style>

1
src/Avalonia.Themes.Default/ComboBox.xaml

@ -45,7 +45,6 @@
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
MemberSelector="{TemplateBinding MemberSelector}"
VirtualizationMode="{TemplateBinding VirtualizationMode}"
/>
</ScrollViewer>

2
src/Avalonia.Themes.Default/DataValidationErrors.xaml

@ -29,7 +29,7 @@
</Style>
</Canvas.Styles>
<ToolTip.Tip>
<ItemsControl Items="{Binding}" MemberSelector="Message"/>
<ItemsControl Items="{Binding}"/>
</ToolTip.Tip>
<Path Data="M14,7 A7,7 0 0,0 0,7 M0,7 A7,7 0 1,0 14,7 M7,3l0,5 M7,9l0,2" Stroke="{DynamicResource ErrorBrush}" StrokeThickness="2"/>
</Canvas>

5
src/Avalonia.Themes.Default/ItemsControl.xaml

@ -4,8 +4,7 @@
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
MemberSelector="{TemplateBinding MemberSelector}"/>
ItemTemplate="{TemplateBinding ItemTemplate}"/>
</ControlTemplate>
</Setter>
</Style>
</Style>

3
src/Avalonia.Themes.Default/ListBox.xaml

@ -18,10 +18,9 @@
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{TemplateBinding Padding}"
MemberSelector="{TemplateBinding MemberSelector}"
VirtualizationMode="{TemplateBinding VirtualizationMode}"/>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter>
</Style>
</Style>

6
src/Avalonia.Themes.Default/MenuItem.xaml

@ -56,8 +56,7 @@
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="2"
MemberSelector="{TemplateBinding MemberSelector}"/>
Margin="2"/>
<Rectangle Name="iconSeparator"
Fill="{DynamicResource ThemeControlMidBrush}"
HorizontalAlignment="Left"
@ -114,8 +113,7 @@
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="2"
MemberSelector="{TemplateBinding MemberSelector}"/>
Margin="2"/>
<Rectangle Name="iconSeparator"
Fill="{DynamicResource ThemeControlMidBrush}"
HorizontalAlignment="Left"

3
src/Avalonia.Themes.Default/TabControl.xaml

@ -14,8 +14,7 @@
Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
MemberSelector="{TemplateBinding MemberSelector}" >
ItemTemplate="{TemplateBinding ItemTemplate}">
</ItemsPresenter>
<ContentPresenter
Name="PART_SelectedContentHost"

3
src/Avalonia.Themes.Default/TabStrip.xaml

@ -3,7 +3,6 @@
<Setter Property="Template">
<ControlTemplate>
<ItemsPresenter Name="PART_ItemsPresenter"
MemberSelector="{TemplateBinding MemberSelector}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"/>
@ -18,4 +17,4 @@
<Style Selector="TabStrip > TabStripItem">
<Setter Property="Margin" Value="16"/>
</Style>
</Styles>
</Styles>

3
src/Avalonia.Themes.Default/TreeView.xaml

@ -15,8 +15,7 @@
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{TemplateBinding Padding}"
MemberSelector="{TemplateBinding MemberSelector}"/>
Margin="{TemplateBinding Padding}"/>
</ScrollViewer>
</Border>
</ControlTemplate>

3
src/Avalonia.Themes.Default/TreeViewItem.xaml

@ -32,8 +32,7 @@
<ItemsPresenter Name="PART_ItemsPresenter"
IsVisible="{TemplateBinding IsExpanded}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
MemberSelector="{TemplateBinding MemberSelector}"/>
ItemsPanel="{TemplateBinding ItemsPanel}"/>
</StackPanel>
</ControlTemplate>
</Setter>

5
src/Avalonia.Visuals/Rendering/RenderLayers.cs

@ -8,8 +8,8 @@ namespace Avalonia.Rendering
{
public class RenderLayers : IEnumerable<RenderLayer>
{
private List<RenderLayer> _inner = new List<RenderLayer>();
private Dictionary<IVisual, RenderLayer> _index = new Dictionary<IVisual, RenderLayer>();
private readonly List<RenderLayer> _inner = new List<RenderLayer>();
private readonly Dictionary<IVisual, RenderLayer> _index = new Dictionary<IVisual, RenderLayer>();
public int Count => _inner.Count;
public RenderLayer this[IVisual layerRoot] => _index[layerRoot];
@ -56,6 +56,7 @@ namespace Avalonia.Rendering
}
_index.Clear();
_inner.Clear();
}
public bool TryGetValue(IVisual layerRoot, out RenderLayer value)

2
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -12,7 +12,6 @@
<Compile Include="AvaloniaXamlLoader.cs" />
<Compile Include="Converters\AvaloniaUriTypeConverter.cs" />
<Compile Include="Converters\FontFamilyTypeConverter.cs" />
<Compile Include="Converters\MemberSelectorTypeConverter.cs" />
<Compile Include="Converters\TimeSpanTypeConverter.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="MarkupExtension.cs" />
@ -33,7 +32,6 @@
<Compile Include="Templates\DataTemplate.cs" />
<Compile Include="Templates\FocusAdornerTemplate.cs" />
<Compile Include="Templates\ItemsPanelTemplate.cs" />
<Compile Include="Templates\MemberSelector.cs" />
<Compile Include="Templates\Template.cs" />
<Compile Include="Templates\TemplateContent.cs" />
<Compile Include="Templates\TreeDataTemplate.cs" />

24
src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs

@ -1,24 +0,0 @@
// 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;
using System.Globalization;
using Avalonia.Markup.Xaml.Templates;
namespace Avalonia.Markup.Xaml.Converters
{
using System.ComponentModel;
public class MemberSelectorTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return MemberSelector.Parse((string)value);
}
}
}

48
src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs

@ -1,48 +0,0 @@
// 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 Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using System;
using System.Reactive.Linq;
namespace Avalonia.Markup.Xaml.Templates
{
public class MemberSelector : IMemberSelector
{
private string _memberName;
public string MemberName
{
get { return _memberName; }
set
{
if (_memberName != value)
{
_memberName = value;
}
}
}
public static MemberSelector Parse(string s)
{
return new MemberSelector { MemberName = s };
}
public object Select(object o)
{
if (string.IsNullOrEmpty(MemberName))
{
return o;
}
var expression = ExpressionObserverBuilder.Build(o, MemberName);
object result = AvaloniaProperty.UnsetValue;
expression.Subscribe(x => result = x);
return (result == AvaloniaProperty.UnsetValue || result is BindingNotification) ? null : result;
}
}
}

2
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs

@ -103,8 +103,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
var ilist = typeSystem.GetType("System.Collections.Generic.IList`1");
AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")),
typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter"));
Add("Avalonia.Controls.Templates.IMemberSelector",
"Avalonia.Markup.Xaml.Converters.MemberSelectorTypeConverter");
Add("Avalonia.Controls.WindowIcon","Avalonia.Markup.Xaml.Converters.IconTypeConverter");
Add("System.Globalization.CultureInfo", "System.ComponentModel.CultureInfoConverter");
Add("System.Uri", "Avalonia.Markup.Xaml.Converters.AvaloniaUriTypeConverter");

78
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Base.UnitTests
@ -34,10 +35,10 @@ namespace Avalonia.Base.UnitTests
{
var target = new Class1();
target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(6));
target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.Error));
target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(7));
target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(6));
target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.Error));
target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(7));
Assert.Equal(
new[]
@ -73,7 +74,7 @@ namespace Avalonia.Base.UnitTests
var source = new Subject<object>();
var target = new Class1
{
[!Class1.ValidatedDirectProperty] = source.ToBinding(),
[!Class1.ValidatedDirectIntProperty] = source.ToBinding(),
};
source.OnNext(new BindingNotification(6));
@ -92,6 +93,30 @@ namespace Avalonia.Base.UnitTests
target.Notifications.AsEnumerable());
}
[Fact]
public void Bound_Validated_Direct_String_Property_Can_Be_Set_To_Null()
{
var source = new ViewModel
{
StringValue = "foo",
};
var target = new Class1
{
[!Class1.ValidatedDirectStringProperty] = new Binding
{
Path = nameof(ViewModel.StringValue),
Source = source,
},
};
Assert.Equal("foo", target.ValidatedDirectString);
source.StringValue = null;
Assert.Null(target.ValidatedDirectString);
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<int> NonValidatedProperty =
@ -104,15 +129,23 @@ namespace Avalonia.Base.UnitTests
o => o.NonValidatedDirect,
(o, v) => o.NonValidatedDirect = v);
public static readonly DirectProperty<Class1, int> ValidatedDirectProperty =
public static readonly DirectProperty<Class1, int> ValidatedDirectIntProperty =
AvaloniaProperty.RegisterDirect<Class1, int>(
nameof(ValidatedDirect),
o => o.ValidatedDirect,
(o, v) => o.ValidatedDirect = v,
nameof(ValidatedDirectInt),
o => o.ValidatedDirectInt,
(o, v) => o.ValidatedDirectInt = v,
enableDataValidation: true);
public static readonly DirectProperty<Class1, string> ValidatedDirectStringProperty =
AvaloniaProperty.RegisterDirect<Class1, string>(
nameof(ValidatedDirectString),
o => o.ValidatedDirectString,
(o, v) => o.ValidatedDirectString = v,
enableDataValidation: true);
private int _nonValidatedDirect;
private int _direct;
private int _directInt;
private string _directString;
public int NonValidated
{
@ -122,14 +155,20 @@ namespace Avalonia.Base.UnitTests
public int NonValidatedDirect
{
get { return _direct; }
get { return _directInt; }
set { SetAndRaise(NonValidatedDirectProperty, ref _nonValidatedDirect, value); }
}
public int ValidatedDirect
public int ValidatedDirectInt
{
get { return _directInt; }
set { SetAndRaise(ValidatedDirectIntProperty, ref _directInt, value); }
}
public string ValidatedDirectString
{
get { return _direct; }
set { SetAndRaise(ValidatedDirectProperty, ref _direct, value); }
get { return _directString; }
set { SetAndRaise(ValidatedDirectStringProperty, ref _directString, value); }
}
public IList<BindingNotification> Notifications { get; } = new List<BindingNotification>();
@ -139,5 +178,16 @@ namespace Avalonia.Base.UnitTests
Notifications.Add(notification);
}
}
public class ViewModel : NotifyingBase
{
private string _stringValue;
public string StringValue
{
get { return _stringValue; }
set { _stringValue = value; RaisePropertyChanged(); }
}
}
}
}

54
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -16,6 +16,60 @@ namespace Avalonia.Controls.UnitTests
private Mock<IPopupImpl> popupImpl;
private MouseTestHelper _mouse = new MouseTestHelper();
[Fact]
public void Opening_Raises_Single_Opened_Event()
{
using (Application())
{
var sut = new ContextMenu();
var target = new Panel
{
ContextMenu = sut
};
new Window { Content = target };
int openedCount = 0;
sut.MenuOpened += (sender, args) =>
{
openedCount++;
};
sut.Open(null);
Assert.Equal(1, openedCount);
}
}
[Fact]
public void Closing_Raises_Single_Closed_Event()
{
using (Application())
{
var sut = new ContextMenu();
var target = new Panel
{
ContextMenu = sut
};
new Window { Content = target };
sut.Open(null);
int closedCount = 0;
sut.MenuClosed += (sender, args) =>
{
closedCount++;
};
sut.Close();
Assert.Equal(1, closedCount);
}
}
[Fact]
public void Clicking_On_Control_Toggles_ContextMenu()
{

6
tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs

@ -118,7 +118,7 @@ namespace Avalonia.Controls.UnitTests.Generators
{
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var container = (ContentPresenter)target.Materialize(0, "foo", null).ContainerControl;
var container = (ContentPresenter)target.Materialize(0, "foo").ContainerControl;
Assert.Equal("foo", container.Content);
@ -135,7 +135,7 @@ namespace Avalonia.Controls.UnitTests.Generators
{
var owner = new Decorator();
var target = new ItemContainerGenerator<ListBoxItem>(owner, ListBoxItem.ContentProperty, null);
var container = (ListBoxItem)target.Materialize(0, "foo", null).ContainerControl;
var container = (ListBoxItem)target.Materialize(0, "foo").ContainerControl;
Assert.Equal("foo", container.Content);
@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests.Generators
foreach (var item in items)
{
var container = generator.Materialize(index++, item, null);
var container = generator.Materialize(index++, item);
result.Add(container);
}

2
tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs

@ -35,7 +35,7 @@ namespace Avalonia.Controls.UnitTests.Generators
foreach (var item in items)
{
var container = generator.Materialize(index++, item, null);
var container = generator.Materialize(index++, item);
result.Add(container);
}

22
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -430,27 +430,6 @@ namespace Avalonia.Controls.UnitTests
dataContexts);
}
[Fact]
public void MemberSelector_Should_Select_Member()
{
var target = new ItemsControl
{
Template = GetTemplate(),
Items = new[] { new Item("Foo"), new Item("Bar") },
MemberSelector = new FuncMemberSelector<Item, string>(x => x.Value),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var text = target.Presenter.Panel.Children
.Cast<ContentPresenter>()
.Select(x => x.Content)
.ToList();
Assert.Equal(new[] { "Foo", "Bar" }, text);
}
[Fact]
public void Control_Item_Should_Not_Be_NameScope()
{
@ -563,7 +542,6 @@ namespace Avalonia.Controls.UnitTests
Child = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
MemberSelector = parent.MemberSelector,
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
}.RegisterInNameScope(scope)
};

40
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs

@ -310,46 +310,6 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Equal(target.Panel, child);
}
[Fact]
public void MemberSelector_Should_Select_Member()
{
var target = new ItemsPresenter
{
Items = new[] { new Item("Foo"), new Item("Bar") },
MemberSelector = new FuncMemberSelector<Item, string>(x => x.Value),
};
target.ApplyTemplate();
var text = target.Panel.Children
.Cast<ContentPresenter>()
.Select(x => x.Content)
.ToList();
Assert.Equal(new[] { "Foo", "Bar" }, text);
}
[Fact]
public void MemberSelector_Should_Set_DataContext()
{
var items = new[] { new Item("Foo"), new Item("Bar") };
var target = new ItemsPresenter
{
Items = items,
MemberSelector = new FuncMemberSelector<Item, string>(x => x.Value),
};
target.ApplyTemplate();
var dataContexts = target.Panel.Children
.Cast<ContentPresenter>()
.Do(x => x.UpdateChild())
.Select(x => x.DataContext)
.ToList();
Assert.Equal(new[] { "Foo", "Bar" }, dataContexts);
}
private class Item
{
public Item(string value)

48
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@ -3,6 +3,7 @@
using System;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using Moq;
using Avalonia.Controls.Presenters;
@ -185,6 +186,53 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
}
[Fact]
public void Popup_Open_Should_Raise_Single_Opened_Event()
{
using (CreateServices())
{
var window = new Window();
var target = new Popup();
window.Content = target;
int openedCount = 0;
target.Opened += (sender, args) =>
{
openedCount++;
};
target.Open();
Assert.Equal(1, openedCount);
}
}
[Fact]
public void Popup_Close_Should_Raise_Single_Closed_Event()
{
using (CreateServices())
{
var window = new Window();
var target = new Popup();
window.Content = target;
target.Open();
int closedCount = 0;
target.Closed += (sender, args) =>
{
closedCount++;
};
target.Close();
Assert.Equal(1, closedCount);
}
}
[Fact]
public void PopupRoot_Should_Have_Template_Applied()
{

115
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -53,7 +53,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
[Fact]
public void Assigning_SelectedItems_Should_Set_SelectedIndex()
public void Assigning_Single_SelectedItems_Should_Set_SelectedIndex()
{
var target = new TestSelector
{
@ -62,9 +62,51 @@ namespace Avalonia.Controls.UnitTests.Primitives
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectedItems = new AvaloniaList<object>("bar");
Assert.Equal(1, target.SelectedIndex);
Assert.Equal(new[] { "bar" }, target.SelectedItems);
Assert.Equal(new[] { 1 }, SelectedContainers(target));
}
[Fact]
public void Assigning_Multiple_SelectedItems_Should_Set_SelectedIndex()
{
// Note that we don't need SelectionMode = Multiple here. Multiple selections can always
// be made in code.
var target = new TestSelector
{
Items = new[] { "foo", "bar", "baz" },
Template = Template(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectedItems = new AvaloniaList<string>("foo", "bar", "baz");
Assert.Equal(0, target.SelectedIndex);
Assert.Equal(new[] { "foo", "bar", "baz" }, target.SelectedItems);
Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target));
}
[Fact]
public void Selected_Items_Should_Be_Marked_When_Panel_Created_After_SelectedItems_Is_Set()
{
// Issue #2565.
var target = new TestSelector
{
Items = new[] { "foo", "bar", "baz" },
Template = Template(),
};
target.ApplyTemplate();
target.SelectedItems = new AvaloniaList<string>("foo", "bar", "baz");
target.Presenter.ApplyTemplate();
Assert.Equal(0, target.SelectedIndex);
Assert.Equal(new[] { "foo", "bar", "baz" }, target.SelectedItems);
Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target));
}
[Fact]
@ -1026,6 +1068,71 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(1, target.SelectedItems.Count);
}
[Fact]
public void Adding_Selected_ItemContainers_Should_Update_Selection()
{
var items = new AvaloniaList<ItemContainer>(new[]
{
new ItemContainer(),
new ItemContainer(),
});
var target = new TestSelector
{
Items = items,
Template = Template(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
items.Add(new ItemContainer { IsSelected = true });
items.Add(new ItemContainer { IsSelected = true });
Assert.Equal(2, target.SelectedIndex);
Assert.Equal(items[2], target.SelectedItem);
Assert.Equal(new[] { items[2], items[3] }, target.SelectedItems);
}
[Fact]
public void Shift_Right_Click_Should_Not_Select_Multiple()
{
var target = new ListBox
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz" },
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Width = 20, Height = 10 }),
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
_helper.Click((Interactive)target.Presenter.Panel.Children[0]);
_helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: InputModifiers.Shift);
Assert.Equal(1, target.SelectedItems.Count);
}
[Fact]
public void Ctrl_Right_Click_Should_Not_Select_Multiple()
{
var target = new ListBox
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz" },
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Width = 20, Height = 10 }),
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
_helper.Click((Interactive)target.Presenter.Panel.Children[0]);
_helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: InputModifiers.Control);
Assert.Equal(1, target.SelectedItems.Count);
}
private IEnumerable<int> SelectedContainers(SelectingItemsControl target)
{
return target.Presenter.Panel.Children
@ -1078,5 +1185,11 @@ namespace Avalonia.Controls.UnitTests.Primitives
public List<string> Items { get; }
public List<string> SelectedItems { get; }
}
private class ItemContainer : Control, ISelectable
{
public string Value { get; set; }
public bool IsSelected { get; set; }
}
}
}

55
tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs

@ -14,60 +14,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
public class TabStripTests
{
[Fact]
public void Header_Of_IHeadered_Items_Should_Be_Used()
{
var items = new[]
{
#pragma warning disable CS0252 // Possible unintended reference comparison; left hand side needs cast
Mock.Of<IHeadered>(x => x.Header == "foo"),
Mock.Of<IHeadered>(x => x.Header == "bar"),
#pragma warning restore CS0252 // Possible unintended reference comparison; left hand side needs cast
};
var target = new TabStrip
{
Template = new FuncControlTemplate<TabStrip>(CreateTabStripTemplate),
Items = items,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var result = target.GetLogicalChildren()
.OfType<TabStripItem>()
.Select(x => x.Content)
.ToList();
Assert.Equal(new[] { "foo", "bar" }, result);
}
[Fact]
public void Data_Of_Non_IHeadered_Items_Should_Be_Used()
{
var items = new[]
{
"foo",
"bar"
};
var target = new TabStrip
{
Template = new FuncControlTemplate<TabStrip>(CreateTabStripTemplate),
Items = items,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var result = target.GetLogicalChildren()
.OfType<TabStripItem>()
.Select(x => x.Content)
.ToList();
Assert.Equal(new[] { "foo", "bar" }, result);
}
[Fact]
public void First_Tab_Should_Be_Selected_By_Default()
{
@ -165,7 +111,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
Name = "itemsPresenter",
[!ItemsPresenter.ItemsProperty] = parent[!ItemsControl.ItemsProperty],
[!ItemsPresenter.MemberSelectorProperty] = parent[!ItemsControl.MemberSelectorProperty],
}.RegisterInNameScope(scope);
}
}

23
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -444,6 +444,22 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Setting_Bound_Text_To_Null_Works()
{
using (UnitTestApplication.Start(Services))
{
var source = new Class1 { Bar = "bar" };
var target = new TextBox { DataContext = source };
target.Bind(TextBox.TextProperty, new Binding("Bar"));
Assert.Equal("bar", target.Text);
source.Bar = null;
Assert.Null(target.Text);
}
}
private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
@ -492,12 +508,19 @@ namespace Avalonia.Controls.UnitTests
private class Class1 : NotifyingBase
{
private int _foo;
private string _bar;
public int Foo
{
get { return _foo; }
set { _foo = value; RaisePropertyChanged(); }
}
public string Bar
{
get { return _bar; }
set { _bar = value; RaisePropertyChanged(); }
}
}
}
}

125
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -11,6 +11,7 @@ using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.UnitTests;
using Xunit;
@ -719,6 +720,129 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection()
{
var tree = CreateTestTreeData();
var target = new TreeView
{
Template = CreateTreeViewTemplate(),
Items = tree,
SelectionMode = SelectionMode.Multiple,
};
var visualRoot = new TestRoot();
visualRoot.Child = target;
CreateNodeDataTemplate(target);
ApplyTemplates(target);
target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]);
target.SelectAll();
AssertChildrenSelected(target, tree[0]);
Assert.Equal(5, target.SelectedItems.Count);
_mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right);
Assert.Equal(5, target.SelectedItems.Count);
}
[Fact]
public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection()
{
var tree = CreateTestTreeData();
var target = new TreeView
{
Template = CreateTreeViewTemplate(),
Items = tree,
SelectionMode = SelectionMode.Multiple,
};
var visualRoot = new TestRoot();
visualRoot.Child = target;
CreateNodeDataTemplate(target);
ApplyTemplates(target);
target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]);
var rootNode = tree[0];
var to = rootNode.Children[0];
var then = rootNode.Children[1];
var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode);
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
var thenContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(then);
ClickContainer(fromContainer, InputModifiers.None);
ClickContainer(toContainer, InputModifiers.Shift);
Assert.Equal(2, target.SelectedItems.Count);
_mouse.Click(thenContainer, MouseButton.Right);
Assert.Equal(1, target.SelectedItems.Count);
}
[Fact]
public void Shift_Right_Click_Should_Not_Select_Multiple()
{
var tree = CreateTestTreeData();
var target = new TreeView
{
Template = CreateTreeViewTemplate(),
Items = tree,
SelectionMode = SelectionMode.Multiple,
};
var visualRoot = new TestRoot();
visualRoot.Child = target;
CreateNodeDataTemplate(target);
ApplyTemplates(target);
target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]);
var rootNode = tree[0];
var from = rootNode.Children[0];
var to = rootNode.Children[1];
var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
_mouse.Click(fromContainer);
_mouse.Click(toContainer, MouseButton.Right, modifiers: InputModifiers.Shift);
Assert.Equal(1, target.SelectedItems.Count);
}
[Fact]
public void Ctrl_Right_Click_Should_Not_Select_Multiple()
{
var tree = CreateTestTreeData();
var target = new TreeView
{
Template = CreateTreeViewTemplate(),
Items = tree,
SelectionMode = SelectionMode.Multiple,
};
var visualRoot = new TestRoot();
visualRoot.Child = target;
CreateNodeDataTemplate(target);
ApplyTemplates(target);
target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]);
var rootNode = tree[0];
var from = rootNode.Children[0];
var to = rootNode.Children[1];
var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
_mouse.Click(fromContainer);
_mouse.Click(toContainer, MouseButton.Right, modifiers: InputModifiers.Control);
Assert.Equal(1, target.SelectedItems.Count);
}
private void ApplyTemplates(TreeView tree)
{
tree.ApplyTemplate();
@ -853,7 +977,6 @@ namespace Avalonia.Controls.UnitTests
}
}
private class Node : NotifyingBase
{
private IAvaloniaList<Node> _children;

55
tests/Avalonia.LeakTests/MemberSelectorTests.cs

@ -1,55 +0,0 @@
using Avalonia.Markup.Xaml.Templates;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using JetBrains.dotMemoryUnit;
using Xunit;
using Xunit.Abstractions;
namespace Avalonia.LeakTests
{
[DotMemoryUnit(FailIfRunWithoutSupport = false)]
public class MemberSelectorTests
{
public MemberSelectorTests(ITestOutputHelper atr)
{
DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine);
}
[Fact]
public void Should_Not_Hold_Reference_To_Object()
{
WeakReference dataRef = null;
var selector = new MemberSelector() { MemberName = "Child.StringValue" };
Action run = () =>
{
var data = new Item()
{
Child = new Item() { StringValue = "Value1" }
};
Assert.Same("Value1", selector.Select(data));
dataRef = new WeakReference(data);
};
run();
GC.Collect();
Assert.False(dataRef.IsAlive);
}
private class Item
{
public Item Child { get; set; }
public int IntValue { get; set; }
public string StringValue { get; set; }
}
}
}

159
tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs

@ -1,159 +0,0 @@
// 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 Avalonia.Markup.Xaml.Templates;
using System;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Templates
{
public class MemberSelectorTests
{
[Fact]
public void Should_Select_Child_Property_Value()
{
var selector = new MemberSelector() { MemberName = "Child.StringValue" };
var data = new Item()
{
Child = new Item() { StringValue = "Value1" }
};
Assert.Same("Value1", selector.Select(data));
}
[Fact]
public void Should_Select_Child_Property_Value_In_Multiple_Items()
{
var selector = new MemberSelector() { MemberName = "Child.StringValue" };
var data = new Item[]
{
new Item() { Child = new Item() { StringValue = "Value1" } },
new Item() { Child = new Item() { StringValue = "Value2" } },
new Item() { Child = new Item() { StringValue = "Value3" } }
};
Assert.Same("Value1", selector.Select(data[0]));
Assert.Same("Value2", selector.Select(data[1]));
Assert.Same("Value3", selector.Select(data[2]));
}
[Fact]
public void Should_Select_MoreComplex_Property_Value()
{
var selector = new MemberSelector() { MemberName = "Child.Child.Child.StringValue" };
var data = new Item()
{
Child = new Item()
{
Child = new Item()
{
Child = new Item() { StringValue = "Value1" }
}
}
};
Assert.Same("Value1", selector.Select(data));
}
[Fact]
public void Should_Select_Null_Value_On_Null_Object()
{
var selector = new MemberSelector() { MemberName = "StringValue" };
Assert.Null(selector.Select(null));
}
[Fact]
public void Should_Select_Null_Value_On_Wrong_MemberName()
{
var selector = new MemberSelector() { MemberName = "WrongProperty" };
var data = new Item() { StringValue = "Value1" };
Assert.Null(selector.Select(data));
}
[Fact]
public void Should_Select_Simple_Property_Value()
{
var selector = new MemberSelector() { MemberName = "StringValue" };
var data = new Item() { StringValue = "Value1" };
Assert.Same("Value1", selector.Select(data));
}
[Fact]
public void Should_Select_Simple_Property_Value_In_Multiple_Items()
{
var selector = new MemberSelector() { MemberName = "StringValue" };
var data = new Item[]
{
new Item() { StringValue = "Value1" },
new Item() { StringValue = "Value2" },
new Item() { StringValue = "Value3" }
};
Assert.Same("Value1", selector.Select(data[0]));
Assert.Same("Value2", selector.Select(data[1]));
Assert.Same("Value3", selector.Select(data[2]));
}
[Fact]
public void Should_Select_Target_On_Empty_MemberName()
{
var selector = new MemberSelector();
var data = new Item() { StringValue = "Value1" };
Assert.Same(data, selector.Select(data));
}
[Fact]
public void Should_Support_Change_Of_MemberName()
{
var selector = new MemberSelector() { MemberName = "StringValue" };
var data = new Item()
{
StringValue = "Value1",
IntValue = 1
};
Assert.Same("Value1", selector.Select(data));
selector.MemberName = "IntValue";
Assert.Equal(1, selector.Select(data));
}
[Fact]
public void Should_Support_Change_Of_Target_Value()
{
var selector = new MemberSelector() { MemberName = "StringValue" };
var data = new Item()
{
StringValue = "Value1"
};
Assert.Same("Value1", selector.Select(data));
data.StringValue = "Value2";
Assert.Same("Value2", selector.Select(data));
}
private class Item
{
public Item Child { get; set; }
public int IntValue { get; set; }
public string StringValue { get; set; }
}
}
}

1
tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs

@ -106,7 +106,6 @@ namespace Avalonia.ReactiveUI.UnitTests
Child = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
MemberSelector = parent.MemberSelector,
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
}.RegisterInNameScope(scope)
};

Loading…
Cancel
Save