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.Xaml\\Avalonia.Markup.Xaml.csproj",
"src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj", "src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj",
"src\\Skia\\Avalonia.Skia\\Avalonia.Skia.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.Direct2D1\\Avalonia.Direct2D1.csproj",
"src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj", "src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.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.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
"tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",
@ -66,4 +65,4 @@
"tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj"
] ]
} }
} }

29
samples/ControlCatalog/Pages/TabControlPage.xaml

@ -4,7 +4,27 @@
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:viewModels="using:ControlCatalog.ViewModels" xmlns:viewModels="using:ControlCatalog.ViewModels"
x:DataType="viewModels:TabControlPageViewModel"> 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 <TextBlock
DockPanel.Dock="Top" DockPanel.Dock="Top"
Classes="h2" Classes="h2"
@ -55,14 +75,14 @@
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}">
<TabControl.ContentTemplate> <TabControl.DataTemplates>
<DataTemplate x:DataType="viewModels:TabControlPageViewModelItem"> <DataTemplate x:DataType="viewModels:TabControlPageViewModelItem">
<StackPanel Orientation="Vertical" Spacing="8"> <StackPanel Orientation="Vertical" Spacing="8">
<TextBlock Text="{Binding Text}"/> <TextBlock Text="{Binding Text}"/>
<Image Source="{Binding Image}" Width="300"/> <Image Source="{Binding Image}" Width="300"/>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
</TabControl.ContentTemplate> </TabControl.DataTemplates>
<TabControl.Styles> <TabControl.Styles>
<Style Selector="TabItem" x:DataType="viewModels:TabControlPageViewModelItem"> <Style Selector="TabItem" x:DataType="viewModels:TabControlPageViewModelItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/> <Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
@ -78,12 +98,13 @@
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center"> VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center">Tab Placement:</TextBlock> <TextBlock VerticalAlignment="Center">Tab Placement:</TextBlock>
<ComboBox SelectedIndex="{Binding TabPlacement, Mode=TwoWay}"> <ComboBox SelectedIndex="{Binding TabPlacement, Mode=TwoWay}" Width="100">
<ComboBoxItem>Left</ComboBoxItem> <ComboBoxItem>Left</ComboBoxItem>
<ComboBoxItem>Bottom</ComboBoxItem> <ComboBoxItem>Bottom</ComboBoxItem>
<ComboBoxItem>Right</ComboBoxItem> <ComboBoxItem>Right</ComboBoxItem>
<ComboBoxItem>Top</ComboBoxItem> <ComboBoxItem>Top</ComboBoxItem>
</ComboBox> </ComboBox>
<CheckBox Name="UseContentTemplates">Set TabItem.ContentTemplate</CheckBox>
</StackPanel> </StackPanel>
</Grid> </Grid>
</DockPanel> </DockPanel>

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

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

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

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

3
src/Avalonia.Controls/GridSplitter.cs

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

2
src/Avalonia.Controls/NativeMenuItemSeparator.cs

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

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

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

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

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

64
src/Avalonia.Controls/TabControl.cs

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

42
src/Avalonia.Controls/TabItem.cs

@ -1,22 +1,28 @@
using System;
using Avalonia.Automation; using Avalonia.Automation;
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Reactive;
using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// An item in a <see cref="TabStrip"/> or <see cref="TabControl"/>. /// An item in a <see cref="TabControl"/>.
/// </summary> /// </summary>
[PseudoClasses(":pressed", ":selected")] [PseudoClasses(":pressed", ":selected")]
public class TabItem : HeaderedContentControl, ISelectable public class TabItem : HeaderedContentControl, ISelectable
{ {
private Dock? _tabStripPlacement;
private IDisposable? _ownerSubscriptions;
/// <summary> /// <summary>
/// Defines the <see cref="TabStripPlacement"/> property. /// Defines the <see cref="TabStripPlacement"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<Dock> TabStripPlacementProperty = public static readonly DirectProperty<TabItem, Dock?> TabStripPlacementProperty =
TabControl.TabStripPlacementProperty.AddOwner<TabItem>(); AvaloniaProperty.RegisterDirect<TabItem, Dock?>(nameof(TabStripPlacement), o => o.TabStripPlacement);
/// <summary> /// <summary>
/// Defines the <see cref="IsSelected"/> property. /// Defines the <see cref="IsSelected"/> property.
@ -37,16 +43,12 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Gets the tab strip placement. /// Gets the placement of this tab relative to the outer <see cref="TabControl"/>, if there is one.
/// </summary> /// </summary>
/// <value> public Dock? TabStripPlacement
/// 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
{ {
get { return GetValue(TabStripPlacementProperty); } get => _tabStripPlacement;
private set => SetAndRaise(TabStripPlacementProperty, ref _tabStripPlacement, value);
} }
/// <summary> /// <summary>
@ -60,6 +62,24 @@ namespace Avalonia.Controls
protected override AutomationPeer OnCreateAutomationPeer() => new ListItemAutomationPeer(this); 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) private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj)
{ {
if (Header == null) if (Header == null)

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

@ -375,6 +375,27 @@ namespace Avalonia.Controls.UnitTests
Assert.Single(target.GetLogicalChildren(), content); 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] [Fact]
public void Should_Not_Propagate_DataContext_To_TabItem_Content() public void Should_Not_Propagate_DataContext_To_TabItem_Content()
{ {

Loading…
Cancel
Save