Browse Source

Merge pull request #11366 from Enscape/fixes/tabitem-contenttemplate-analyzer-warnings

Made TabControl observe changes which affect SelectedContent/Template, plus various analyzer warning fixes
pull/11495/head
Benedikt Stebner 3 years ago
committed by GitHub
parent
commit
65339723d3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      Avalonia.Desktop.slnf
  2. 29
      samples/ControlCatalog/Pages/TabControlPage.xaml
  3. 2
      src/Avalonia.Controls/Calendar/CalendarButton.cs
  4. 2
      src/Avalonia.Controls/Calendar/CalendarDayButton.cs
  5. 3
      src/Avalonia.Controls/GridSplitter.cs
  6. 2
      src/Avalonia.Controls/NativeMenuItemSeparator.cs
  7. 1
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  8. 2
      src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
  9. 64
      src/Avalonia.Controls/TabControl.cs
  10. 42
      src/Avalonia.Controls/TabItem.cs
  11. 21
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs

11
Avalonia.Desktop.slnf

@ -39,14 +39,13 @@
"src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj",
"src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj",
"src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj",
"src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
"src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
"src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj",
"src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj",
"src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
"src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"src\\tools\\Avalonia.Analyzers\\Avalonia.Analyzers.csproj",
"src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
"src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj",
"tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
"tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",
@ -66,4 +65,4 @@
"tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj"
]
}
}
}

29
samples/ControlCatalog/Pages/TabControlPage.xaml

@ -4,7 +4,27 @@
xmlns="https://github.com/avaloniaui"
xmlns:viewModels="using:ControlCatalog.ViewModels"
x:DataType="viewModels:TabControlPageViewModel">
<DockPanel>
<DockPanel Classes.WithContentTemplates="{Binding IsChecked, ElementName=UseContentTemplates}">
<DockPanel.Styles>
<Style Selector="DockPanel.WithContentTemplates">
<Style Selector="^ TabItem">
<Setter Property="ContentTemplate">
<DataTemplate x:CompileBindings="False">
<Border BorderBrush="Red" BorderThickness="10">
<ContentPresenter Content="{Binding}"/>
</Border>
</DataTemplate>
</Setter>
</Style>
<Style Selector="^ TabControl">
<Setter Property="ContentTemplate">
<DataTemplate>
<TextBlock Text="This template should be overriden by each TabItem's template."/>
</DataTemplate>
</Setter>
</Style>
</Style>
</DockPanel.Styles>
<TextBlock
DockPanel.Dock="Top"
Classes="h2"
@ -55,14 +75,14 @@
Margin="0 16"
DisplayMemberBinding="{Binding Header, x:DataType=viewModels:TabControlPageViewModelItem}"
TabStripPlacement="{Binding TabPlacement}">
<TabControl.ContentTemplate>
<TabControl.DataTemplates>
<DataTemplate x:DataType="viewModels:TabControlPageViewModelItem">
<StackPanel Orientation="Vertical" Spacing="8">
<TextBlock Text="{Binding Text}"/>
<Image Source="{Binding Image}" Width="300"/>
</StackPanel>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl.DataTemplates>
<TabControl.Styles>
<Style Selector="TabItem" x:DataType="viewModels:TabControlPageViewModelItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
@ -78,12 +98,13 @@
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center">Tab Placement:</TextBlock>
<ComboBox SelectedIndex="{Binding TabPlacement, Mode=TwoWay}">
<ComboBox SelectedIndex="{Binding TabPlacement, Mode=TwoWay}" Width="100">
<ComboBoxItem>Left</ComboBoxItem>
<ComboBoxItem>Bottom</ComboBoxItem>
<ComboBoxItem>Right</ComboBoxItem>
<ComboBoxItem>Top</ComboBoxItem>
</ComboBox>
<CheckBox Name="UseContentTemplates">Set TabItem.ContentTemplate</CheckBox>
</StackPanel>
</Grid>
</DockPanel>

2
src/Avalonia.Controls/Calendar/CalendarButton.cs

@ -39,7 +39,7 @@ namespace Avalonia.Controls.Primitives
public CalendarButton()
: base()
{
Content = DateTimeHelper.GetCurrentDateFormat().AbbreviatedMonthNames[0];
SetCurrentValue(ContentProperty, DateTimeHelper.GetCurrentDateFormat().AbbreviatedMonthNames[0]);
}
/// <summary>

2
src/Avalonia.Controls/Calendar/CalendarDayButton.cs

@ -34,7 +34,7 @@ namespace Avalonia.Controls.Primitives
: base()
{
//Focusable = false;
Content = DefaultContent.ToString(CultureInfo.CurrentCulture);
SetCurrentValue(ContentProperty, DefaultContent.ToString(CultureInfo.CurrentCulture));
}
/// <summary>

3
src/Avalonia.Controls/GridSplitter.cs

@ -695,7 +695,8 @@ namespace Avalonia.Controls
{
private readonly TranslateTransform _translation;
private readonly Decorator _decorator;
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", Justification = "Private object")]
public PreviewAdorner(Control? previewControl)
{
// Add a decorator to perform translations.

2
src/Avalonia.Controls/NativeMenuItemSeparator.cs

@ -4,7 +4,7 @@
{
public NativeMenuItemSeparator()
{
Header = "-";
SetCurrentValue(HeaderProperty, "-");
}
}
}

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

@ -29,6 +29,7 @@ namespace Avalonia.Controls.Primitives
}
/// <inheritdoc />
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1012", Justification = "Explicit set")]
public void SetChild(Control? control)
{
Content = control;

2
src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs

@ -193,7 +193,7 @@ namespace Avalonia.Controls
UpdateContent();
};
Content = _content;
SetCurrentValue(ContentProperty, _content);
}
else
{

64
src/Avalonia.Controls/TabControl.cs

@ -10,6 +10,7 @@ using Avalonia.LogicalTree;
using Avalonia.VisualTree;
using Avalonia.Automation;
using Avalonia.Controls.Metadata;
using Avalonia.Reactive;
namespace Avalonia.Controls
{
@ -19,6 +20,10 @@ namespace Avalonia.Controls
[TemplatePart("PART_ItemsPresenter", typeof(ItemsPresenter))]
public class TabControl : SelectingItemsControl, IContentPresenterHost
{
private object? _selectedContent;
private IDataTemplate? _selectedContentTemplate;
private CompositeDisposable? _selectedItemSubscriptions;
/// <summary>
/// Defines the <see cref="TabStripPlacement"/> property.
/// </summary>
@ -46,14 +51,14 @@ namespace Avalonia.Controls
/// <summary>
/// The selected content property
/// </summary>
public static readonly StyledProperty<object?> SelectedContentProperty =
AvaloniaProperty.Register<TabControl, object?>(nameof(SelectedContent));
public static readonly DirectProperty<TabControl, object?> SelectedContentProperty =
AvaloniaProperty.RegisterDirect<TabControl, object?>(nameof(SelectedContent), o => o.SelectedContent);
/// <summary>
/// The selected content template property
/// </summary>
public static readonly StyledProperty<IDataTemplate?> SelectedContentTemplateProperty =
AvaloniaProperty.Register<TabControl, IDataTemplate?>(nameof(SelectedContentTemplate));
public static readonly DirectProperty<TabControl, IDataTemplate?> SelectedContentTemplateProperty =
AvaloniaProperty.RegisterDirect<TabControl, IDataTemplate?>(nameof(SelectedContentTemplate), o => o.SelectedContentTemplate);
/// <summary>
/// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
@ -115,11 +120,10 @@ namespace Avalonia.Controls
/// <value>
/// The content of the selected tab.
/// </value>
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1032", Justification = "This property is supposed to be a styled readonly property.")]
public object? SelectedContent
{
get { return GetValue(SelectedContentProperty); }
internal set { SetValue(SelectedContentProperty, value); }
get => _selectedContent;
internal set => SetAndRaise(SelectedContentProperty, ref _selectedContent, value);
}
/// <summary>
@ -128,11 +132,10 @@ namespace Avalonia.Controls
/// <value>
/// The content template of the selected tab.
/// </value>
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1032", Justification = "This property is supposed to be a styled readonly property.")]
public IDataTemplate? SelectedContentTemplate
{
get { return GetValue(SelectedContentTemplateProperty); }
internal set { SetValue(SelectedContentTemplateProperty, value); }
get => _selectedContentTemplate;
internal set => SetAndRaise(SelectedContentTemplateProperty, ref _selectedContentTemplate, value);
}
internal ItemsPresenter? ItemsPresenterPart { get; private set; }
@ -161,18 +164,10 @@ namespace Avalonia.Controls
protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{
base.PrepareContainerForItemOverride(element, item, index);
if (element is TabItem tabItem)
{
if (ContentTemplate is { } ct)
tabItem.ContentTemplate = ct;
tabItem.SetValue(TabStripPlacementProperty, TabStripPlacement);
}
if (index == SelectedIndex && element is ContentControl container)
if (index == SelectedIndex)
{
SelectedContentTemplate = container.ContentTemplate;
SelectedContent = container.Content;
UpdateSelectedContent(element);
}
}
@ -192,18 +187,25 @@ namespace Avalonia.Controls
UpdateSelectedContent();
}
private void UpdateSelectedContent()
private void UpdateSelectedContent(Control? container = null)
{
_selectedItemSubscriptions?.Dispose();
_selectedItemSubscriptions = null;
if (SelectedIndex == -1)
{
SelectedContent = SelectedContentTemplate = null;
}
else
{
var container = SelectedItem as IContentControl ??
ContainerFromIndex(SelectedIndex) as IContentControl;
SelectedContentTemplate = container?.ContentTemplate;
SelectedContent = container?.Content;
container ??= ContainerFromIndex(SelectedIndex);
if (container != null)
{
_selectedItemSubscriptions = new CompositeDisposable(
container.GetObservable(ContentControl.ContentProperty).Subscribe(v => SelectedContent = v),
// Note how we fall back to our own ContentTemplate if the container doesn't specify one
container.GetObservable(ContentControl.ContentTemplateProperty).Subscribe(v => SelectedContentTemplate = v ?? ContentTemplate));
}
}
}
@ -224,7 +226,7 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
ItemsPresenterPart = e.NameScope.Get<ItemsPresenter>("PART_ItemsPresenter");
ItemsPresenterPart = e.NameScope.Find<ItemsPresenter>("PART_ItemsPresenter");
ItemsPresenterPart?.ApplyTemplate();
// Set TabNavigation to Once on the panel if not already set and
@ -285,6 +287,16 @@ namespace Avalonia.Controls
{
RefreshContainers();
}
else if (change.Property == ContentTemplateProperty)
{
var newTemplate = change.GetNewValue<IDataTemplate?>();
if (SelectedContentTemplate != newTemplate &&
ContainerFromIndex(SelectedIndex) is { } container &&
container.GetValue(ContentControl.ContentTemplateProperty) == null)
{
SelectedContentTemplate = newTemplate; // See also UpdateSelectedContent
}
}
else if (change.Property == KeyboardNavigation.TabOnceActiveElementProperty &&
ItemsPresenterPart?.Panel is { } panel)
{

42
src/Avalonia.Controls/TabItem.cs

@ -1,22 +1,28 @@
using System;
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Reactive;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
/// <summary>
/// An item in a <see cref="TabStrip"/> or <see cref="TabControl"/>.
/// An item in a <see cref="TabControl"/>.
/// </summary>
[PseudoClasses(":pressed", ":selected")]
public class TabItem : HeaderedContentControl, ISelectable
{
private Dock? _tabStripPlacement;
private IDisposable? _ownerSubscriptions;
/// <summary>
/// Defines the <see cref="TabStripPlacement"/> property.
/// </summary>
public static readonly StyledProperty<Dock> TabStripPlacementProperty =
TabControl.TabStripPlacementProperty.AddOwner<TabItem>();
public static readonly DirectProperty<TabItem, Dock?> TabStripPlacementProperty =
AvaloniaProperty.RegisterDirect<TabItem, Dock?>(nameof(TabStripPlacement), o => o.TabStripPlacement);
/// <summary>
/// Defines the <see cref="IsSelected"/> property.
@ -37,16 +43,12 @@ namespace Avalonia.Controls
}
/// <summary>
/// Gets the tab strip placement.
/// Gets the placement of this tab relative to the outer <see cref="TabControl"/>, if there is one.
/// </summary>
/// <value>
/// The tab strip placement.
/// </value>
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1031",
Justification = "This property is supposed to be inherited only and settable on parent TabControl.")]
public Dock TabStripPlacement
public Dock? TabStripPlacement
{
get { return GetValue(TabStripPlacementProperty); }
get => _tabStripPlacement;
private set => SetAndRaise(TabStripPlacementProperty, ref _tabStripPlacement, value);
}
/// <summary>
@ -60,6 +62,24 @@ namespace Avalonia.Controls
protected override AutomationPeer OnCreateAutomationPeer() => new ListItemAutomationPeer(this);
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_ownerSubscriptions?.Dispose();
_ownerSubscriptions = null;
if (this.FindAncestorOfType<TabControl>() is { } owner && owner.IndexFromContainer(this) != -1)
{
SubscribeToOwnerProperties(owner);
}
}
protected void SubscribeToOwnerProperties(AvaloniaObject owner)
{
_ownerSubscriptions = owner.GetObservable(TabControl.TabStripPlacementProperty).Subscribe(v => TabStripPlacement = v);
}
private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj)
{
if (Header == null)

21
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -375,6 +375,27 @@ namespace Avalonia.Controls.UnitTests
Assert.Single(target.GetLogicalChildren(), content);
}
[Fact]
public void SelectedContentTemplate_Updates_After_New_ContentTemplate()
{
TabControl target = new TabControl
{
Template = TabControlTemplate(),
ItemsSource = new[] { "Foo" },
};
var root = new TestRoot(target);
ApplyTemplate(target);
((ContentPresenter)target.ContentPart).UpdateChild();
Assert.Equal(null, Assert.IsType<TextBlock>(target.ContentPart.Child).Tag);
target.ContentTemplate = new FuncDataTemplate<string>((x, _) =>
new TextBlock { Tag = "bar", Text = x });
Assert.Equal("bar", Assert.IsType<TextBlock>(target.ContentPart.Child).Tag);
}
[Fact]
public void Should_Not_Propagate_DataContext_To_TabItem_Content()
{

Loading…
Cancel
Save