Browse Source

ContentPresenter should create child without content, if template was set (#6226)

* Fix #6224

* Fix data templates Match

* Do not use preview features

* Do not create Child if Content is null and DataTemplate was set

* Update src/Avalonia.Base/Utilities/TypeUtilities.cs

* Update src/Avalonia.Controls/Presenters/ContentPresenter.cs

* Update src/Avalonia.Controls/Presenters/ContentPresenter.cs
Max Katz 4 years ago
parent
commit
0f83ccb4b0

+ 29 - 3
samples/ControlCatalog/Pages/ComboBoxPage.xaml

@@ -1,12 +1,20 @@
 <UserControl xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="ControlCatalog.Pages.ComboBoxPage"
-             xmlns:sys="clr-namespace:System;assembly=netstandard">
+             xmlns:sys="using:System"
+             xmlns:col="using:System.Collections">
   <StackPanel Orientation="Vertical" Spacing="4">
     <TextBlock Classes="h1">ComboBox</TextBlock>
     <TextBlock Classes="h2">A drop-down list.</TextBlock>
 
-    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Spacing="8">
+    <WrapPanel HorizontalAlignment="Center" Margin="0 16 0 0"
+               MaxWidth="750">
+      <WrapPanel.Styles>
+        <Style Selector="ComboBox">
+          <Setter Property="Width" Value="250" />
+          <Setter Property="Margin" Value="10" />
+        </Style>
+      </WrapPanel.Styles>
       <ComboBox PlaceholderText="Pick an Item">
         <ComboBoxItem>Inline Items</ComboBoxItem>
         <ComboBoxItem>Inline Item 2</ComboBoxItem>
@@ -14,6 +22,24 @@
         <ComboBoxItem>Inline Item 4</ComboBoxItem>
       </ComboBox>
 
+      <ComboBox>
+        <ComboBox.Items>
+          <col:ArrayList>
+            <x:Null />
+            <sys:String>Hello</sys:String>
+            <sys:String>World</sys:String>
+          </col:ArrayList>
+        </ComboBox.Items>
+        <ComboBox.ItemTemplate>
+          <DataTemplate>
+            <Panel>
+              <TextBlock Text="{Binding}" />
+              <TextBlock Text="Null object" IsVisible="{Binding Converter={x:Static ObjectConverters.IsNull}}" />
+            </Panel>
+          </DataTemplate>
+        </ComboBox.ItemTemplate>
+      </ComboBox>
+
       <ComboBox SelectedIndex="0">
         <ComboBoxItem>
           <Panel>
@@ -46,7 +72,7 @@
           <sys:Exception /> 
         </DataValidationErrors.Error>
       </ComboBox>
-    </StackPanel>
+    </WrapPanel>
 
   </StackPanel>
 </UserControl>

+ 23 - 11
src/Avalonia.Base/Utilities/TypeUtilities.cs

@@ -93,13 +93,25 @@ namespace Avalonia.Utilities
             return !type.IsValueType || IsNullableType(type);
         }
 
+        /// <summary>
+        /// Returns a value indicating whether value can be casted to the specified type.
+        /// If value is null, checks if instances of that type can be null.
+        /// </summary>
+        /// <typeparam name="T">The type to cast to</typeparam>
+        /// <param name="value">The value to check if cast possible</param>
+        /// <returns>True if the cast is possible, otherwise false.</returns>
+        public static bool CanCast<T>(object value)
+        {
+            return value is T || (value is null && AcceptsNull(typeof(T)));
+        }
+
         /// <summary>
         /// Try to convert a value to a type by any means possible.
         /// </summary>
-        /// <param name="to">The type to cast to.</param>
-        /// <param name="value">The value to cast.</param>
+        /// <param name="to">The type to convert to.</param>
+        /// <param name="value">The value to convert.</param>
         /// <param name="culture">The culture to use.</param>
-        /// <param name="result">If successful, contains the cast value.</param>
+        /// <param name="result">If successful, contains the convert value.</param>
         /// <returns>True if the cast was successful, otherwise false.</returns>
         public static bool TryConvert(Type to, object value, CultureInfo culture, out object result)
         {
@@ -216,10 +228,10 @@ namespace Avalonia.Utilities
         /// Try to convert a value to a type using the implicit conversions allowed by the C#
         /// language.
         /// </summary>
-        /// <param name="to">The type to cast to.</param>
-        /// <param name="value">The value to cast.</param>
-        /// <param name="result">If successful, contains the cast value.</param>
-        /// <returns>True if the cast was successful, otherwise false.</returns>
+        /// <param name="to">The type to convert to.</param>
+        /// <param name="value">The value to convert.</param>
+        /// <param name="result">If successful, contains the converted value.</param>
+        /// <returns>True if the convert was successful, otherwise false.</returns>
         public static bool TryConvertImplicit(Type to, object value, out object result)
         {
             if (value == null)
@@ -278,8 +290,8 @@ namespace Avalonia.Utilities
         /// Convert a value to a type by any means possible, returning the default for that type
         /// if the value could not be converted.
         /// </summary>
-        /// <param name="value">The value to cast.</param>
-        /// <param name="type">The type to cast to..</param>
+        /// <param name="value">The value to convert.</param>
+        /// <param name="type">The type to convert to..</param>
         /// <param name="culture">The culture to use.</param>
         /// <returns>A value of <paramref name="type"/>.</returns>
         public static object ConvertOrDefault(object value, Type type, CultureInfo culture)
@@ -291,8 +303,8 @@ namespace Avalonia.Utilities
         /// Convert a value to a type using the implicit conversions allowed by the C# language or
         /// return the default for the type if the value could not be converted.
         /// </summary>
-        /// <param name="value">The value to cast.</param>
-        /// <param name="type">The type to cast to..</param>
+        /// <param name="value">The value to convert.</param>
+        /// <param name="type">The type to convert to.</param>
         /// <returns>A value of <paramref name="type"/>.</returns>
         public static object ConvertImplicitOrDefault(object value, Type type)
         {

+ 5 - 1
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -327,7 +327,11 @@ namespace Avalonia.Controls.Presenters
             var oldChild = Child;
             var newChild = content as IControl;
 
-            if (content != null && newChild == null)
+            // We want to allow creating Child from the Template, if Content is null.
+            // But it's important to not use DataTemplates, otherwise we will break content presenters in many places,
+            // otherwise it will blow up every ContentPresenter without Content set.
+            if (newChild == null
+                && (content != null || ContentTemplate != null))
             {
                 var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? 
                     (

+ 4 - 2
src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs

@@ -1,5 +1,7 @@
 using System;
 
+using Avalonia.Utilities;
+
 namespace Avalonia.Controls.Templates
 {
     /// <summary>
@@ -16,7 +18,7 @@ namespace Avalonia.Controls.Templates
         /// </param>
         /// <param name="supportsRecycling">Whether the control can be recycled.</param>
         public FuncDataTemplate(Func<T, INameScope, IControl> build, bool supportsRecycling = false)
-            : base(typeof(T), CastBuild(build), supportsRecycling)
+            : base(o => TypeUtilities.CanCast<T>(o), CastBuild(build), supportsRecycling)
         {
         }
 
@@ -63,7 +65,7 @@ namespace Avalonia.Controls.Templates
         /// <returns>The weakly typed function.</returns>
         private static Func<object, bool> CastMatch(Func<T, bool> f)
         {
-            return o => (o is T) && f((T)o);
+            return o => TypeUtilities.CanCast<T>(o) && f((T)o);
         }
 
         /// <summary>

+ 0 - 1
src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs

@@ -9,7 +9,6 @@ namespace Avalonia.Markup.Xaml.Templates
     {
         public Type DataType { get; set; }
 
-        //we need content to be object otherwise portable.xaml is crashing
         [Content]
         [TemplateContent]
         public object Content { get; set; }

+ 86 - 0
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs

@@ -264,5 +264,91 @@ namespace Avalonia.Controls.UnitTests.Presenters
             // InheritanceParent is exposed via StylingParent.
             Assert.Same(logicalParent, ((IStyledElement)child).StylingParent);
         }
+
+        [Fact]
+        public void Should_Create_Child_Even_With_Null_Content_When_ContentTemplate_Is_Set()
+        {
+            var target = new ContentPresenter
+            {
+                ContentTemplate = new FuncDataTemplate<object>(_ => true, (_, __) => new TextBlock
+                {
+                    Text = "Hello World"
+                }),
+                Content = null
+            };
+
+            target.UpdateChild();
+
+            var textBlock = Assert.IsType<TextBlock>(target.Child);
+            Assert.Equal("Hello World", textBlock.Text);
+        }
+
+        [Fact]
+        public void Should_Not_Create_Child_Even_With_Null_Content_And_DataTemplates_InsteadOf_ContentTemplate()
+        {
+            var target = new ContentPresenter
+            {
+                DataTemplates =
+                {
+                    new FuncDataTemplate<object>(_ => true, (_, __) => new TextBlock
+                    {
+                        Text = "Hello World"
+                    })
+                },
+                Content = null
+            };
+
+            target.UpdateChild();
+
+            Assert.Null(target.Child);
+        }
+
+        [Fact]
+        public void Should_Not_Create_Child_When_Content_And_Template_Are_Null()
+        {
+            var target = new ContentPresenter
+            {
+                ContentTemplate = null,
+                Content = null
+            };
+
+            target.UpdateChild();
+
+            Assert.Null(target.Child);
+        }
+
+        [Fact]
+        public void Should_Not_Create_When_Child_Content_Is_Null_But_Expected_ValueType_With_FuncDataTemplate()
+        {
+            var target = new ContentPresenter
+            {
+                ContentTemplate = new FuncDataTemplate<int>(_ => true, (_, __) => new TextBlock
+                {
+                    Text = "Hello World"
+                }),
+                Content = null
+            };
+
+            target.UpdateChild();
+
+            Assert.Null(target.Child);
+        }
+
+        [Fact]
+        public void Should_Create_Child_When_Content_Is_Null_And_Expected_NullableValueType_With_FuncDataTemplate()
+        {
+            var target = new ContentPresenter
+            {
+                ContentTemplate = new FuncDataTemplate<int?>(_ => true, (_, __) => new TextBlock
+                {
+                    Text = "Hello World"
+                }),
+                Content = null
+            };
+
+            target.UpdateChild();
+
+            Assert.NotNull(target.Child);
+        }
     }
 }