diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml index 025b85492c..d440b7cce3 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml @@ -1,12 +1,20 @@ + xmlns:sys="using:System" + xmlns:col="using:System.Collections"> ComboBox A drop-down list. - + + + + Inline Items Inline Item 2 @@ -14,6 +22,24 @@ Inline Item 4 + + + + + Hello + World + + + + + + + + + + + + @@ -46,7 +72,7 @@ - + diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 9f2308a062..179ded3549 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -93,13 +93,25 @@ namespace Avalonia.Utilities return !type.IsValueType || IsNullableType(type); } + /// + /// 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. + /// + /// The type to cast to + /// The value to check if cast possible + /// True if the cast is possible, otherwise false. + public static bool CanCast(object value) + { + return value is T || (value is null && AcceptsNull(typeof(T))); + } + /// /// Try to convert a value to a type by any means possible. /// - /// The type to cast to. - /// The value to cast. + /// The type to convert to. + /// The value to convert. /// The culture to use. - /// If successful, contains the cast value. + /// If successful, contains the convert value. /// True if the cast was successful, otherwise false. 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. /// - /// The type to cast to. - /// The value to cast. - /// If successful, contains the cast value. - /// True if the cast was successful, otherwise false. + /// The type to convert to. + /// The value to convert. + /// If successful, contains the converted value. + /// True if the convert was successful, otherwise false. 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. /// - /// The value to cast. - /// The type to cast to.. + /// The value to convert. + /// The type to convert to.. /// The culture to use. /// A value of . 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. /// - /// The value to cast. - /// The type to cast to.. + /// The value to convert. + /// The type to convert to. /// A value of . public static object ConvertImplicitOrDefault(object value, Type type) { diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 472727823a..1a46d84558 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/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) ?? ( diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs index 4a6a1c6cfb..8e7b290247 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs @@ -1,5 +1,7 @@ using System; +using Avalonia.Utilities; + namespace Avalonia.Controls.Templates { /// @@ -16,7 +18,7 @@ namespace Avalonia.Controls.Templates /// /// Whether the control can be recycled. public FuncDataTemplate(Func build, bool supportsRecycling = false) - : base(typeof(T), CastBuild(build), supportsRecycling) + : base(o => TypeUtilities.CanCast(o), CastBuild(build), supportsRecycling) { } @@ -63,7 +65,7 @@ namespace Avalonia.Controls.Templates /// The weakly typed function. private static Func CastMatch(Func f) { - return o => (o is T) && f((T)o); + return o => TypeUtilities.CanCast(o) && f((T)o); } /// diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index 650534b347..b7db1a3fbb 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/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; } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index c7aa583b6f..6b744ed79c 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs +++ b/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(_ => true, (_, __) => new TextBlock + { + Text = "Hello World" + }), + Content = null + }; + + target.UpdateChild(); + + var textBlock = Assert.IsType(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(_ => 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(_ => 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(_ => true, (_, __) => new TextBlock + { + Text = "Hello World" + }), + Content = null + }; + + target.UpdateChild(); + + Assert.NotNull(target.Child); + } } }