diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
index ec88852feb..e52430f50b 100644
--- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
+++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
@@ -9,42 +9,37 @@
1.0
apk
true
+ android-arm64;android-x64
-
-
-
Resources\drawable\Icon.png
-
- False
- False
+
True
+
+
+
True
no-write-symbols,nodebug
Hybrid
True
-
- False
- False
-
-
-
- True
+
+ True
+ True
-
-
+
+
-
\ No newline at end of file
+
diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
index aa570ec504..6f551d2b01 100644
--- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
+++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
@@ -1,4 +1,5 @@
-
+
+
diff --git a/samples/ControlCatalog.Android/environment.device.txt b/samples/ControlCatalog.Android/environment.device.txt
new file mode 100644
index 0000000000..107d68ca1b
--- /dev/null
+++ b/samples/ControlCatalog.Android/environment.device.txt
@@ -0,0 +1 @@
+DOTNET_DiagnosticPorts=127.0.0.1:9000,suspend
diff --git a/samples/ControlCatalog.Android/environment.emulator.txt b/samples/ControlCatalog.Android/environment.emulator.txt
new file mode 100644
index 0000000000..299a0ec30b
--- /dev/null
+++ b/samples/ControlCatalog.Android/environment.emulator.txt
@@ -0,0 +1 @@
+DOTNET_DiagnosticPorts=10.0.2.2:9001,suspend
diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs
index 216e43e1f0..784d33ed58 100644
--- a/src/Avalonia.Controls/GridSplitter.cs
+++ b/src/Avalonia.Controls/GridSplitter.cs
@@ -221,7 +221,8 @@ namespace Avalonia.Controls
ShowsPreview = showsPreview,
ResizeDirection = resizeDirection,
SplitterLength = Math.Min(Bounds.Width, Bounds.Height),
- ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection)
+ ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection),
+ Scaling = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1,
};
// Store the rows and columns to resize on drag events.
@@ -630,13 +631,17 @@ namespace Avalonia.Controls
{
double actualLength1 = GetActualLength(definition1);
double actualLength2 = GetActualLength(definition2);
+ double pixelLength = 1 / _resizeData.Scaling;
+ double epsilon = pixelLength + LayoutHelper.LayoutEpsilon;
// When splitting, Check to see if the total pixels spanned by the definitions
- // is the same as before starting resize. If not cancel the drag.
+ // is the same as before starting resize. If not cancel the drag. We need to account for
+ // layout rounding here, so ignore differences of less than a device pixel to avoid problems
+ // that WPF has, such as https://stackoverflow.com/questions/28464843.
if (_resizeData.SplitBehavior == SplitBehavior.Split &&
!MathUtilities.AreClose(
actualLength1 + actualLength2,
- _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength, LayoutHelper.LayoutEpsilon))
+ _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength, epsilon))
{
CancelResize();
@@ -798,6 +803,9 @@ namespace Avalonia.Controls
// The minimum of Width/Height of Splitter. Used to ensure splitter
// isn't hidden by resizing a row/column smaller than the splitter.
public double SplitterLength;
+
+ // The current layout scaling factor.
+ public double Scaling;
}
}
diff --git a/src/Avalonia.Controls/Templates/DataTemplates.cs b/src/Avalonia.Controls/Templates/DataTemplates.cs
index f203539536..d4eeda7908 100644
--- a/src/Avalonia.Controls/Templates/DataTemplates.cs
+++ b/src/Avalonia.Controls/Templates/DataTemplates.cs
@@ -1,3 +1,4 @@
+using System;
using Avalonia.Collections;
namespace Avalonia.Controls.Templates
@@ -13,6 +14,22 @@ namespace Avalonia.Controls.Templates
public DataTemplates()
{
ResetBehavior = ResetBehavior.Remove;
+
+ Validate += ValidateDataTemplate;
+ }
+
+ private static void ValidateDataTemplate(IDataTemplate template)
+ {
+ var valid = template switch
+ {
+ ITypedDataTemplate typed => typed.DataType is not null,
+ _ => true
+ };
+
+ if (!valid)
+ {
+ throw new InvalidOperationException("DataTemplate inside of DataTemplates must have a DataType set. Set DataType property or use ItemTemplate with single template instead.");
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Templates/ITypedDataTemplate.cs b/src/Avalonia.Controls/Templates/ITypedDataTemplate.cs
new file mode 100644
index 0000000000..239dbd79f4
--- /dev/null
+++ b/src/Avalonia.Controls/Templates/ITypedDataTemplate.cs
@@ -0,0 +1,10 @@
+using System;
+using Avalonia.Metadata;
+
+namespace Avalonia.Controls.Templates;
+
+public interface ITypedDataTemplate : IDataTemplate
+{
+ [DataType]
+ Type? DataType { get; }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
index 1ca7be67a7..04a61e5f10 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
@@ -35,7 +35,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
Transformers.Insert(2, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer());
Transformers.Insert(3, _bindingTransformer = new AvaloniaBindingExtensionTransformer());
-
// Targeted
InsertBefore(
new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
@@ -57,6 +56,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer()
);
+ InsertAfter(
+ new XDataTypeTransformer());
+
// After everything else
InsertBefore(
new AddNameScopeRegistration(),
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs
new file mode 100644
index 0000000000..845dc5f831
--- /dev/null
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs
@@ -0,0 +1,73 @@
+using System.Collections.Generic;
+using System.Linq;
+using XamlX;
+using XamlX.Ast;
+using XamlX.Transform;
+using XamlX.Transform.Transformers;
+using XamlX.TypeSystem;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
+{
+ internal class XDataTypeTransformer : IXamlAstTransformer
+ {
+ private const string DataTypePropertyName = "DataType";
+
+ ///
+ /// Converts x:DataType directives to regular DataType assignments if property with Avalonia.Metadata.DataTypeAttribute exists.
+ ///
+ ///
+ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
+ {
+ if (node is XamlAstObjectNode on)
+ {
+ for (var c = 0; c < on.Children.Count; c++)
+ {
+ var ch = on.Children[c];
+ if (ch is XamlAstXmlDirective { Namespace: XamlNamespaces.Xaml2006, Name: DataTypePropertyName } d)
+ {
+ if (on.Children.OfType()
+ .Any(p => ((XamlAstNamePropertyReference)p.Property)?.Name == DataTypePropertyName))
+ {
+ // Break iteration if any DataType property was already set by user code.
+ break;
+ }
+
+ var templateDataTypeAttribute = context.GetAvaloniaTypes().DataTypeAttribute;
+
+ var clrType = (on.Type as XamlAstClrTypeReference)?.Type;
+ if (clrType is null)
+ {
+ break;
+ }
+
+ // Technically it's possible to map "x:DataType" to a property with [DataType] attribute regardless of its name,
+ // but we go explicitly strict here and check the name as well.
+ var (declaringType, dataTypeProperty) = GetAllProperties(clrType)
+ .FirstOrDefault(t => t.property.Name == DataTypePropertyName && t.property.CustomAttributes
+ .Any(a => a.Type == templateDataTypeAttribute));
+
+ if (dataTypeProperty is not null)
+ {
+ on.Children[c] = new XamlAstXamlPropertyValueNode(d,
+ new XamlAstNamePropertyReference(d,
+ new XamlAstClrTypeReference(ch, declaringType, false), dataTypeProperty.Name,
+ on.Type),
+ d.Values);
+ }
+ }
+ }
+ }
+
+ return node;
+ }
+
+ private static IEnumerable<(IXamlType declaringType, IXamlProperty property)> GetAllProperties(IXamlType t)
+ {
+ foreach (var p in t.Properties)
+ yield return (t, p);
+ if(t.BaseType!=null)
+ foreach (var tuple in GetAllProperties(t.BaseType))
+ yield return tuple;
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
index d2b24979cc..4da6b1b791 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
@@ -5,7 +5,7 @@ using Avalonia.Metadata;
namespace Avalonia.Markup.Xaml.Templates
{
- public class DataTemplate : IRecyclingDataTemplate
+ public class DataTemplate : IRecyclingDataTemplate, ITypedDataTemplate
{
[DataType]
public Type DataType { get; set; }
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
index 10061c3d48..04e8b0a9c0 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
@@ -9,7 +9,7 @@ using Avalonia.Metadata;
namespace Avalonia.Markup.Xaml.Templates
{
- public class TreeDataTemplate : ITreeDataTemplate
+ public class TreeDataTemplate : ITreeDataTemplate, ITypedDataTemplate
{
[DataType]
public Type DataType { get; set; }
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
index 7e721fd7b2..f3f2d2f1e4 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
@@ -17,6 +17,7 @@ using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.UnitTests;
+using JetBrains.Annotations;
using XamlX;
using Xunit;
@@ -413,11 +414,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:TestDataContext'>
-
+
-
+
";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
@@ -1527,7 +1528,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
[TemplateContent]
public object Content { get; set; }
- public bool Match(object data) => FancyDataType.IsInstanceOfType(data);
+ public bool Match(object data) => FancyDataType?.IsInstanceOfType(data) ?? true;
public IControl Build(object data) => TemplateContent.Load(Content)?.Control;
}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
index 8188b212e1..affa292a7d 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
@@ -74,18 +74,18 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
-
+
-
+
-
+
-
+
";
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs
index 53881467e7..e005964ad0 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs
@@ -1,5 +1,11 @@
+using System;
+using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Markup.Xaml.Templates;
+using Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;
+using Avalonia.Metadata;
using Avalonia.UnitTests;
using Xunit;
@@ -89,6 +95,93 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
+ [Fact]
+ public void XDataType_Should_Be_Assigned_To_Clr_Property()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+
+
+
+";
+ var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+ var target = window.FindControl("target");
+ var template = (DataTemplate)window.DataTemplates.First();
+
+ window.ApplyTemplate();
+ target.ApplyTemplate();
+ ((ContentPresenter)target.Presenter).UpdateChild();
+
+ Assert.Equal(typeof(string), template.DataType);
+ Assert.IsType