소스 검색

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

Made TabControl observe changes which affect SelectedContent/Template, plus various analyzer warning fixes
Benedikt Stebner 2 년 전
부모
커밋
65339723d3

+ 5 - 6
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"
     ]
     ]
   }
   }
-}
+}

+ 25 - 4
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>

+ 1 - 1
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>

+ 1 - 1
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>

+ 2 - 1
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.

+ 1 - 1
src/Avalonia.Controls/NativeMenuItemSeparator.cs

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

+ 1 - 0
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;

+ 1 - 1
src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs

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

+ 38 - 26
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 =
-            AvaloniaProperty.Register<TabControl, object?>(nameof(SelectedContent));
+        public static readonly DirectProperty<TabControl, object?> SelectedContentProperty =
+            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 =
-            AvaloniaProperty.Register<TabControl, IDataTemplate?>(nameof(SelectedContentTemplate));
+        public static readonly DirectProperty<TabControl, IDataTemplate?> SelectedContentTemplateProperty =
+            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); }
-            internal set { SetValue(SelectedContentProperty, value); }
+            get => _selectedContent;
+            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); }
-            internal set { SetValue(SelectedContentTemplateProperty, value); }
+            get => _selectedContentTemplate;
+            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;
-                SelectedContent = container.Content;
+                UpdateSelectedContent(element);
             }
             }
         }
         }
 
 
@@ -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 ??
-                    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)
         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)
             {
             {

+ 31 - 11
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 =
-            TabControl.TabStripPlacementProperty.AddOwner<TabItem>();
+        public static readonly DirectProperty<TabItem, Dock?> TabStripPlacementProperty =
+            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>
-        /// 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>
         /// <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 - 0
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()
         {
         {