Browse Source

Merge pull request #10827 from AvaloniaUI/refactor/readonly-itemscontrol-items

Make ItemsControl.Items readonly.
pull/10874/head
Max Katz 4 years ago
committed by GitHub
parent
commit
7108484eaf
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      samples/BindingDemo/MainWindow.xaml
  2. 2
      samples/ControlCatalog/Pages/TabControlPage.xaml
  3. 8
      samples/VirtualizationDemo/MainWindow.xaml
  4. 2
      src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs
  5. 2
      src/Avalonia.Controls/Flyouts/MenuFlyout.cs
  6. 14
      src/Avalonia.Controls/ItemCollection.cs
  7. 91
      src/Avalonia.Controls/ItemsControl.cs
  8. 2
      src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
  9. 1
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  10. 43
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
  11. 4
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs

4
samples/BindingDemo/MainWindow.xaml

@ -75,11 +75,11 @@
</StackPanel.DataTemplates> </StackPanel.DataTemplates>
<StackPanel Margin="18" Spacing="4" Width="200"> <StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/> <TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/> <ListBox ItemsSource="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
</StackPanel> </StackPanel>
<StackPanel Margin="18" Spacing="4" Width="200"> <StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/> <TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/> <ListBox ItemsSource="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
</StackPanel> </StackPanel>
<ContentControl Content="{ReflectionBinding Selection.SelectedItems[0]}"> <ContentControl Content="{ReflectionBinding Selection.SelectedItems[0]}">
<ContentControl.DataTemplates> <ContentControl.DataTemplates>

2
samples/ControlCatalog/Pages/TabControlPage.xaml

@ -51,7 +51,7 @@
Text="From DataTemplate"> Text="From DataTemplate">
</TextBlock> </TextBlock>
<TabControl <TabControl
Items="{Binding Tabs}" ItemsSource="{Binding Tabs}"
Margin="0 16" Margin="0 16"
DisplayMemberBinding="{Binding Header, x:DataType=viewModels:TabControlPageViewModelItem}" DisplayMemberBinding="{Binding Header, x:DataType=viewModels:TabControlPageViewModelItem}"
TabStripPlacement="{Binding TabPlacement}"> TabStripPlacement="{Binding TabPlacement}">

8
samples/VirtualizationDemo/MainWindow.xaml

@ -11,7 +11,7 @@
Margin="16 0 0 0" Margin="16 0 0 0"
Width="150" Width="150"
Spacing="4"> Spacing="4">
<ComboBox Items="{Binding Orientations}" <ComboBox ItemsSource="{Binding Orientations}"
SelectedItem="{Binding Orientation}"/> SelectedItem="{Binding Orientation}"/>
<TextBox Watermark="Item Count" <TextBox Watermark="Item Count"
UseFloatingWatermark="True" UseFloatingWatermark="True"
@ -26,10 +26,10 @@
UseFloatingWatermark="True" UseFloatingWatermark="True"
Text="{Binding #listBox.Scroll.Viewport, Mode=OneWay}"/> Text="{Binding #listBox.Scroll.Viewport, Mode=OneWay}"/>
<TextBlock>Horiz. ScrollBar</TextBlock> <TextBlock>Horiz. ScrollBar</TextBlock>
<ComboBox Items="{Binding ScrollBarVisibilities}" <ComboBox ItemsSource="{Binding ScrollBarVisibilities}"
SelectedItem="{Binding HorizontalScrollBarVisibility}"/> SelectedItem="{Binding HorizontalScrollBarVisibility}"/>
<TextBlock>Vert. ScrollBar</TextBlock> <TextBlock>Vert. ScrollBar</TextBlock>
<ComboBox Items="{Binding ScrollBarVisibilities}" <ComboBox ItemsSource="{Binding ScrollBarVisibilities}"
SelectedItem="{Binding VerticalScrollBarVisibility}"/> SelectedItem="{Binding VerticalScrollBarVisibility}"/>
<TextBox Watermark="Item to Create" <TextBox Watermark="Item to Create"
UseFloatingWatermark="True" UseFloatingWatermark="True"
@ -44,7 +44,7 @@
</StackPanel> </StackPanel>
<ListBox Name="listBox" <ListBox Name="listBox"
Items="{Binding Items}" ItemsSource="{Binding Items}"
Selection="{Binding Selection}" Selection="{Binding Selection}"
SelectionMode="Multiple" SelectionMode="Multiple"
ScrollViewer.HorizontalScrollBarVisibility="{Binding HorizontalScrollBarVisibility, Mode=TwoWay}" ScrollViewer.HorizontalScrollBarVisibility="{Binding HorizontalScrollBarVisibility, Mode=TwoWay}"

2
src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs

@ -9,7 +9,7 @@ namespace Avalonia.Metadata;
/// A typical usage example is a ListBox control, where <see cref="InheritDataTypeFromItemsAttribute"/> is defined on the ItemTemplate property, /// A typical usage example is a ListBox control, where <see cref="InheritDataTypeFromItemsAttribute"/> is defined on the ItemTemplate property,
/// allowing the template to inherit the data type from the Items collection binding. /// allowing the template to inherit the data type from the Items collection binding.
/// </remarks> /// </remarks>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class InheritDataTypeFromItemsAttribute : Attribute public sealed class InheritDataTypeFromItemsAttribute : Attribute
{ {
/// <summary> /// <summary>

2
src/Avalonia.Controls/Flyouts/MenuFlyout.cs

@ -90,7 +90,7 @@ namespace Avalonia.Controls
{ {
return new MenuFlyoutPresenter return new MenuFlyoutPresenter
{ {
[!ItemsControl.ItemsProperty] = this[!ItemsProperty], [!ItemsControl.ItemsSourceProperty] = this[!ItemsProperty],
[!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty], [!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty],
[!ItemsControl.ItemContainerThemeProperty] = this[!ItemContainerThemeProperty], [!ItemsControl.ItemContainerThemeProperty] = this[!ItemContainerThemeProperty],
}; };

14
src/Avalonia.Controls/ItemCollection.cs

@ -106,19 +106,6 @@ namespace Avalonia.Controls
} }
} }
internal IList? GetItemsPropertyValue()
{
if (_mode == Mode.ObsoleteItemsSetter)
return Source == s_uninitialized ? null : Source;
return this;
}
internal void SetItems(IList? items)
{
_mode = Mode.ObsoleteItemsSetter;
SetSource(items ?? s_uninitialized);
}
internal void SetItemsSource(IEnumerable? value) internal void SetItemsSource(IEnumerable? value)
{ {
if (_mode != Mode.ItemsSource && Count > 0) if (_mode != Mode.ItemsSource && Count > 0)
@ -159,7 +146,6 @@ namespace Avalonia.Controls
{ {
Items, Items,
ItemsSource, ItemsSource,
ObsoleteItemsSetter,
} }
} }
} }

91
src/Avalonia.Controls/ItemsControl.cs

@ -30,17 +30,6 @@ namespace Avalonia.Controls
private static readonly FuncTemplate<Panel?> DefaultPanel = private static readonly FuncTemplate<Panel?> DefaultPanel =
new(() => new StackPanel()); new(() => new StackPanel());
/// <summary>
/// Defines the <see cref="Items"/> property.
/// </summary>
public static readonly DirectProperty<ItemsControl, IList?> ItemsProperty =
AvaloniaProperty.RegisterDirect<ItemsControl, IList?>(
nameof(Items),
o => o.Items,
#pragma warning disable CS0618 // Type or member is obsolete
(o, v) => o.Items = v);
#pragma warning restore CS0618 // Type or member is obsolete
/// <summary> /// <summary>
/// Defines the <see cref="ItemContainerTheme"/> property. /// Defines the <see cref="ItemContainerTheme"/> property.
/// </summary> /// </summary>
@ -94,7 +83,6 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
[AssignBinding] [AssignBinding]
[InheritDataTypeFromItems(nameof(ItemsSource))] [InheritDataTypeFromItems(nameof(ItemsSource))]
[InheritDataTypeFromItems(nameof(Items))]
public IBinding? DisplayMemberBinding public IBinding? DisplayMemberBinding
{ {
get => GetValue(DisplayMemberBindingProperty); get => GetValue(DisplayMemberBindingProperty);
@ -129,48 +117,20 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Gets or sets the items to display. /// Gets the items to display.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Since Avalonia 11, <see cref="ItemsControl"/> has both an <see cref="Items"/> property /// You use either the <see cref="Items"/> or the <see cref="ItemsSource"/> property to
/// and an <see cref="ItemsSource"/> property. The properties have the following differences: /// specify the collection that should be used to generate the content of your
/// /// <see cref="ItemsControl"/>. When the <see cref="ItemsSource"/> property is set, the
/// <list type="bullet"> /// <see cref="Items"/> collection is made read-only and fixed-size.
/// <item><see cref="Items"/> is initialized with an empty collection and is a direct property, ///
/// meaning that it cannot be styled </item> /// When <see cref="ItemsSource"/> is in use, setting the <see cref="ItemsSource"/>
/// <item><see cref="ItemsSource"/> is by default null, and is a styled property. This property /// property to null removes the collection and restores usage to <see cref="Items"/>,
/// is marked as the content property and will be used for items added via inline XAML.</item> /// which will be an empty <see cref="ItemCollection"/>.
/// </list>
///
/// In Avalonia 11 the two properties can be used almost interchangeably but this will change
/// in a later version. In order to be ready for this change, follow the following guidance:
///
/// <list type="bullet">
/// <item>You should use the <see cref="Items"/> property when you're assigning a collection of
/// item containers directly, for example adding a collection of <see cref="ListBoxItem"/>s
/// directly to a <see cref="ListBox"/>. Add the containers to the pre-existing list, do not
/// reassign the <see cref="Items"/> property via the setter or with a binding.</item>
/// <item>You should use the <see cref="ItemsSource"/> property when you're assigning or
/// binding a collection of models which will be transformed by a data template.</item>
/// </list>
/// </remarks> /// </remarks>
[Content] [Content]
public IList? Items public ItemCollection Items => _items;
{
get => _items.GetItemsPropertyValue();
[Obsolete("Use ItemsSource to set or bind items.")]
set
{
var oldItems = _items.GetItemsPropertyValue();
if (value != oldItems)
{
_items.SetItems(value);
RaisePropertyChanged(ItemsProperty, oldItems, value);
}
}
}
/// <summary> /// <summary>
/// Gets or sets the <see cref="ControlTheme"/> that is applied to the container element generated for each item. /// Gets or sets the <see cref="ControlTheme"/> that is applied to the container element generated for each item.
@ -210,27 +170,17 @@ namespace Avalonia.Controls
/// Gets or sets a collection used to generate the content of the <see cref="ItemsControl"/>. /// Gets or sets a collection used to generate the content of the <see cref="ItemsControl"/>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Since Avalonia 11, <see cref="ItemsControl"/> has both an <see cref="Items"/> property /// A common scenario is to use an <see cref="ItemsControl"/> such as a
/// and an <see cref="ItemsSource"/> property. The properties have the following differences: /// <see cref="ListBox"/> to display a data collection, or to bind an
/// /// <see cref="ItemsControl"/> to a collection object. To bind an <see cref="ItemsControl"/>
/// <list type="bullet"> /// to a collection object, use the <see cref="ItemsSource"/> property.
/// <item><see cref="Items"/> is initialized with an empty collection and is a direct property,
/// meaning that it cannot be styled </item>
/// <item><see cref="ItemsSource"/> is by default null, and is a styled property. This property
/// is marked as the content property and will be used for items added via inline XAML.</item>
/// </list>
///
/// In Avalonia 11 the two properties can be used almost interchangeably but this will change
/// in a later version. In order to be ready for this change, follow the following guidance:
/// ///
/// <list type="bullet"> /// When the <see cref="ItemsSource"/> property is set, the <see cref="Items"/> collection
/// <item>You should use the <see cref="Items"/> property when you're assigning a collection of /// is made read-only and fixed-size.
/// item containers directly, for example adding a collection of <see cref="ListBoxItem"/>s ///
/// directly to a <see cref="ListBox"/>. Add the containers to the pre-existing list, do not /// When <see cref="ItemsSource"/> is in use, setting the property to null removes the
/// reassign the <see cref="Items"/> property via the setter or with a binding.</item> /// collection and restores usage to <see cref="Items"/>, which will be an empty
/// <item>You should use the <see cref="ItemsSource"/> property when you're assigning or /// <see cref="ItemCollection"/>.
/// binding a collection of models which will be transformed by a data template.</item>
/// </list>
/// </remarks> /// </remarks>
public IEnumerable? ItemsSource public IEnumerable? ItemsSource
{ {
@ -242,7 +192,6 @@ namespace Avalonia.Controls
/// Gets or sets the data template used to display the items in the control. /// Gets or sets the data template used to display the items in the control.
/// </summary> /// </summary>
[InheritDataTypeFromItems(nameof(ItemsSource))] [InheritDataTypeFromItems(nameof(ItemsSource))]
[InheritDataTypeFromItems(nameof(Items))]
public IDataTemplate? ItemTemplate public IDataTemplate? ItemTemplate
{ {
get => GetValue(ItemTemplateProperty); get => GetValue(ItemTemplateProperty);

2
src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs

@ -124,7 +124,7 @@ namespace Avalonia.Controls.Primitives
treeTemplate.Match(item) && treeTemplate.Match(item) &&
treeTemplate.ItemsSelector(item) is { } itemsBinding) treeTemplate.ItemsSelector(item) is { } itemsBinding)
{ {
_itemsBinding = BindingOperations.Apply(this, ItemsProperty, itemsBinding, null); _itemsBinding = BindingOperations.Apply(this, ItemsSourceProperty, itemsBinding, null);
} }
} }

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

@ -235,7 +235,6 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
[AssignBinding] [AssignBinding]
[InheritDataTypeFromItems(nameof(ItemsSource))] [InheritDataTypeFromItems(nameof(ItemsSource))]
[InheritDataTypeFromItems(nameof(Items))]
public IBinding? SelectedValueBinding public IBinding? SelectedValueBinding
{ {
get => GetValue(SelectedValueBindingProperty); get => GetValue(SelectedValueBindingProperty);

43
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs

@ -73,32 +73,27 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
// Infer data type from collection binding on a control that displays items. // Infer data type from collection binding on a control that displays items.
var property = context.ParentNodes().OfType<XamlPropertyAssignmentNode>().FirstOrDefault(); var property = context.ParentNodes().OfType<XamlPropertyAssignmentNode>().FirstOrDefault();
var attributeType = context.GetAvaloniaTypes().InheritDataTypeFromItemsAttribute; var attributeType = context.GetAvaloniaTypes().InheritDataTypeFromItemsAttribute;
var attributes = property?.Property?.GetClrProperty().CustomAttributes var attribute = property?.Property?.GetClrProperty().CustomAttributes
.Where(a => a.Type == attributeType).ToList(); .FirstOrDefault(a => a.Type == attributeType);
if (attributes?.Count > 0) if (attribute is not null)
{ {
foreach (var attribute in attributes) var propertyName = (string)attribute.Parameters.First();
XamlAstConstructableObjectNode parentObject;
if (attribute.Properties.TryGetValue("AncestorType", out var type)
&& type is IXamlType xamlType)
{ {
var propertyName = (string)attribute.Parameters.First(); parentObject = context.ParentNodes().OfType<XamlAstConstructableObjectNode>()
XamlAstConstructableObjectNode parentObject; .FirstOrDefault(n => n.Type.GetClrType().FullName == xamlType.FullName);
if (attribute.Properties.TryGetValue("AncestorType", out var type) }
&& type is IXamlType xamlType) else
{ {
parentObject = context.ParentNodes().OfType<XamlAstConstructableObjectNode>() parentObject = context.ParentNodes().OfType<XamlAstConstructableObjectNode>().FirstOrDefault();
.FirstOrDefault(n => n.Type.GetClrType().FullName == xamlType.FullName); }
}
else if (parentObject != null)
{ {
parentObject = context.ParentNodes().OfType<XamlAstConstructableObjectNode>().FirstOrDefault(); inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject, propertyName);
}
if (parentObject != null)
{
inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject, propertyName);
if (inferredDataContextTypeNode != null)
break;
}
} }
} }

4
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs

@ -73,14 +73,14 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
var xaml = @" var xaml = @"
<Window xmlns='https://github.com/avaloniaui'> <Window xmlns='https://github.com/avaloniaui'>
<DockPanel> <DockPanel>
<TabStrip Name='strip' DockPanel.Dock='Top' Items='{Binding Items}' SelectedIndex='0'> <TabStrip Name='strip' DockPanel.Dock='Top' ItemsSource='{Binding Items}' SelectedIndex='0'>
<TabStrip.ItemTemplate> <TabStrip.ItemTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text='{Binding Header}'/> <TextBlock Text='{Binding Header}'/>
</DataTemplate> </DataTemplate>
</TabStrip.ItemTemplate> </TabStrip.ItemTemplate>
</TabStrip> </TabStrip>
<Carousel Name='carousel' Items='{Binding Items}' SelectedIndex='{Binding #strip.SelectedIndex}'> <Carousel Name='carousel' ItemsSource='{Binding Items}' SelectedIndex='{Binding #strip.SelectedIndex}'>
<Carousel.ItemTemplate> <Carousel.ItemTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text='{Binding Detail}'/> <TextBlock Text='{Binding Detail}'/>

Loading…
Cancel
Save