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
pull/6466/head
Max Katz 5 years ago
committed by GitHub
parent
commit
0f83ccb4b0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      samples/ControlCatalog/Pages/ComboBoxPage.xaml
  2. 34
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  3. 6
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  4. 6
      src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs
  5. 1
      src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
  6. 86
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs

32
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>

34
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)
{

6
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) ??
(

6
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>

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
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);
}
}
}

Loading…
Cancel
Save