diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets
index 4f9c4d7720..1b822c14cf 100644
--- a/packages/Avalonia/AvaloniaBuildTasks.targets
+++ b/packages/Avalonia/AvaloniaBuildTasks.targets
@@ -74,7 +74,7 @@
ReportImportance="$(AvaloniaXamlReportImportance)"/>
+ Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:GenerateAvaloniaResources /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:BuildProjectReferences=false"/>
@@ -112,7 +112,7 @@
/>
+ Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileAvaloniaXaml /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:BuildProjectReferences=false"/>
diff --git a/samples/BindingDemo/App.xaml b/samples/BindingDemo/App.xaml
index 175e838616..5a8e65ed22 100644
--- a/samples/BindingDemo/App.xaml
+++ b/samples/BindingDemo/App.xaml
@@ -2,8 +2,14 @@
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="BindingDemo.App">
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ExpanderPage.xaml b/samples/ControlCatalog/Pages/ExpanderPage.xaml
index f0a80fd7ab..b5a2e6cdd0 100644
--- a/samples/ControlCatalog/Pages/ExpanderPage.xaml
+++ b/samples/ControlCatalog/Pages/ExpanderPage.xaml
@@ -52,6 +52,24 @@
Rounded
+
+
+ Expanded content
+
+
+
+
+ Expanded content
+
+
diff --git a/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs b/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs
index e8a080899a..98a494e533 100644
--- a/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs
@@ -10,6 +10,12 @@ namespace ControlCatalog.Pages
{
this.InitializeComponent();
DataContext = new ExpanderPageViewModel();
+
+ var CollapsingDisabledExpander = this.Get("CollapsingDisabledExpander");
+ var ExpandingDisabledExpander = this.Get("ExpandingDisabledExpander");
+
+ CollapsingDisabledExpander.Collapsing += (s, e) => { e.Handled = true; };
+ ExpandingDisabledExpander.Expanding += (s, e) => { e.Handled = true; };
}
private void InitializeComponent()
diff --git a/src/Avalonia.Base/Platform/AssetLoader.cs b/src/Avalonia.Base/Platform/AssetLoader.cs
index a74da2a178..8d3576d180 100644
--- a/src/Avalonia.Base/Platform/AssetLoader.cs
+++ b/src/Avalonia.Base/Platform/AssetLoader.cs
@@ -3,16 +3,22 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
+#if !BUILDTASK
using Avalonia.Platform.Internal;
using Avalonia.Utilities;
+#endif
namespace Avalonia.Platform
{
///
/// Loads assets compiled into the application binary.
///
- public class AssetLoader : IAssetLoader
+ public class AssetLoader
+#if !BUILDTASK
+ : IAssetLoader
+#endif
{
+#if !BUILDTASK
private static IAssemblyDescriptorResolver s_assemblyDescriptorResolver = new AssemblyDescriptorResolver();
private AssemblyDescriptor? _defaultResmAssembly;
@@ -206,7 +212,8 @@ namespace Avalonia.Platform
return null;
}
-
+#endif
+
public static void RegisterResUriParsers()
{
if (!UriParser.IsKnownScheme("avares"))
diff --git a/src/Avalonia.Base/Rect.cs b/src/Avalonia.Base/Rect.cs
index 7930228d99..a91b089a33 100644
--- a/src/Avalonia.Base/Rect.cs
+++ b/src/Avalonia.Base/Rect.cs
@@ -243,7 +243,7 @@ namespace Avalonia
}
///
- /// Determines whether a point in in the bounds of the rectangle.
+ /// Determines whether a point is in the bounds of the rectangle.
///
/// The point.
/// true if the point is in the bounds of the rectangle; otherwise false.
diff --git a/src/Avalonia.Base/Utilities/StringBuilderCache.cs b/src/Avalonia.Base/Utilities/StringBuilderCache.cs
index be8b24c848..cbbcf2b398 100644
--- a/src/Avalonia.Base/Utilities/StringBuilderCache.cs
+++ b/src/Avalonia.Base/Utilities/StringBuilderCache.cs
@@ -9,6 +9,7 @@ using System;
using System.Text;
namespace Avalonia.Utilities;
+#nullable enable
// Provide a cached reusable instance of stringbuilder per thread.
internal static class StringBuilderCache
diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
index 74debed828..ce84f23dd4 100644
--- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
+++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
@@ -55,6 +55,9 @@
Markup/%(RecursiveDir)%(FileName)%(Extension)
+
+ Markup\AssetLoader.cs
+
Markup/%(RecursiveDir)%(FileName)%(Extension)
diff --git a/src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs b/src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs
index 43365346b3..a149a758f4 100644
--- a/src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs
+++ b/src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs
@@ -5,6 +5,8 @@ namespace Avalonia.Build.Tasks
InvalidXAML = 1,
DuplicateXClass = 2,
LegacyResmScheme = 3,
+ TransformError = 4,
+ EmitError = 4,
Unknown = 9999
}
diff --git a/src/Avalonia.Build.Tasks/Extensions.cs b/src/Avalonia.Build.Tasks/Extensions.cs
index 46c12eaf3d..9bedacaf52 100644
--- a/src/Avalonia.Build.Tasks/Extensions.cs
+++ b/src/Avalonia.Build.Tasks/Extensions.cs
@@ -7,15 +7,29 @@ namespace Avalonia.Build.Tasks
{
static string FormatErrorCode(BuildEngineErrorCode code) => $"AVLN:{(int)code:0000}";
- public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message)
+ public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, Exception ex,
+ int lineNumber = 0, int linePosition = 0)
{
- engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message,
+#if DEBUG
+ LogError(engine, code, file, ex.ToString(), lineNumber, linePosition);
+#else
+ LogError(engine, code, file, ex.Message, lineNumber, linePosition);
+#endif
+ }
+
+ public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message,
+ int lineNumber = 0, int linePosition = 0)
+ {
+ engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "",
+ lineNumber, linePosition, lineNumber, linePosition, message,
"", "Avalonia"));
}
- public static void LogWarning(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message)
+ public static void LogWarning(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message,
+ int lineNumber = 0, int linePosition = 0)
{
- engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message,
+ engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", FormatErrorCode(code), file ?? "",
+ lineNumber, linePosition, lineNumber, linePosition, message,
"", "Avalonia"));
}
diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
index 974f8485c0..ea2cf2cf99 100644
--- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
+++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions;
+using Avalonia.Platform;
using Microsoft.Build.Framework;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -94,7 +95,7 @@ namespace Avalonia.Build.Tasks
}
catch (Exception ex)
{
- engine.LogError(BuildEngineErrorCode.Unknown, "", ex.Message);
+ engine.LogError(BuildEngineErrorCode.Unknown, "", ex);
return new CompileResult(false);
}
}
@@ -134,6 +135,10 @@ namespace Avalonia.Build.Tasks
engine.LogMessage("Debugging cancelled.", MessageImportance.Normal);
}
}
+
+ // Some transformers might need to parse "avares://" Uri.
+ AssetLoader.RegisterResUriParsers();
+
var asm = typeSystem.TargetAssemblyDefinition;
var avares = new AvaloniaResources(asm, projectDirectory);
if (avares.Resources.Count(CheckXamlName) == 0)
@@ -231,15 +236,14 @@ namespace Avalonia.Build.Tasks
});
asm.MainModule.Types.Add(typeDef);
var builder = typeSystem.CreateTypeBuilder(typeDef);
-
- var populateMethodsToTransform = new List<(MethodDefinition populateMethod, string resourceFilePath)>();
-
- foreach (var res in group.Resources.Where(CheckXamlName).OrderBy(x=>x.FilePath.ToLowerInvariant()))
+
+ IReadOnlyCollection parsedXamlDocuments = new List();
+ foreach (var res in group.Resources.Where(CheckXamlName).OrderBy(x => x.FilePath.ToLowerInvariant()))
{
+ engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", logImportance);
+
try
{
- engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", logImportance);
-
// StreamReader is needed here to handle BOM
var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd();
var parsed = XDocumentXamlParser.Parse(xaml);
@@ -276,9 +280,9 @@ namespace Avalonia.Build.Tasks
compiler.Transform(parsed);
+
var populateName = classType == null ? "Populate:" + res.Name : "!XamlIlPopulate";
- var buildName = classType == null ? "Build:" + res.Name : null;
-
+ var buildName = classType == null ? "Build:" + res.Name : null;
var classTypeDefinition =
classType == null ? null : typeSystem.GetTypeReference(classType).Resolve();
@@ -286,11 +290,57 @@ namespace Avalonia.Build.Tasks
var populateBuilder = classTypeDefinition == null ?
builder :
typeSystem.CreateTypeBuilder(classTypeDefinition);
- compiler.Compile(parsed,
- contextClass,
+
+ ((List)parsedXamlDocuments).Add(new XamlDocumentResource(
+ parsed, res.Uri, res, classType,
+ populateBuilder,
compiler.DefinePopulateMethod(populateBuilder, parsed, populateName,
classTypeDefinition == null),
- buildName == null ? null : compiler.DefineBuildMethod(builder, parsed, buildName, true),
+ buildName == null ? null : compiler.DefineBuildMethod(builder, parsed, buildName, true)));
+ }
+ catch (Exception e)
+ {
+ int lineNumber = 0, linePosition = 0;
+ if (e is XamlParseException xe)
+ {
+ lineNumber = xe.LineNumber;
+ linePosition = xe.LinePosition;
+ }
+
+ engine.LogError(BuildEngineErrorCode.TransformError, res.FilePath, e, lineNumber, linePosition);
+ return false;
+ }
+ }
+
+ try
+ {
+ compiler.TransformGroup(parsedXamlDocuments);
+ }
+ catch (XamlDocumentParseException e)
+ {
+ engine.LogError(BuildEngineErrorCode.TransformError, e.FilePath, e, e.LineNumber, e.LinePosition);
+ }
+ catch (XamlParseException e)
+ {
+ engine.LogError(BuildEngineErrorCode.TransformError, "", e, e.LineNumber, e.LinePosition);
+ }
+
+ foreach (var document in parsedXamlDocuments)
+ {
+ var parsed = document.XamlDocument;
+ var res = (IResource)document.FileSource;
+ var classType = document.ClassType;
+ var populateBuilder = document.TypeBuilder;
+
+ try
+ {
+ var classTypeDefinition =
+ classType == null ? null : typeSystem.GetTypeReference(classType).Resolve();
+
+ compiler.Compile(parsed,
+ contextClass,
+ document.PopulateMethod,
+ document.BuildMethod,
builder.DefineSubType(compilerConfig.WellKnownTypes.Object, "NamespaceInfo:" + res.Name,
true),
(closureName, closureBaseType) =>
@@ -300,23 +350,11 @@ namespace Avalonia.Build.Tasks
res.Uri, res
);
- var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve()
- .Methods.First(m => m.Name == populateName);
-
- // Include populate method and all nested methods/closures used in the populate method,
- // So we can replace style/resource includes in all of them.
- populateMethodsToTransform.Add((compiledPopulateMethod, res.FilePath));
- populateMethodsToTransform.AddRange(compiledPopulateMethod.Body.Instructions
- .Where(b => b.OpCode == OpCodes.Call || b.OpCode == OpCodes.Callvirt || b.OpCode == OpCodes.Ldftn)
- .Select(b => b.Operand)
- .OfType()
- .Where(m => compiledPopulateMethod.DeclaringType.NestedTypes.Contains(m.DeclaringType))
- .Select(m => m.Resolve())
- .Where(m => m.HasBody)
- .Select(m => (m, res.FilePath)));
-
if (classTypeDefinition != null)
{
+ var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve()
+ .Methods.First(m => m.Name == document.PopulateMethod.Name);
+
var designLoaderFieldType = typeSystem
.GetType("System.Action`1")
.MakeGenericType(typeSystem.GetType("System.Object"));
@@ -370,15 +408,11 @@ namespace Avalonia.Build.Tasks
if (i[c].OpCode == OpCodes.Call)
{
var op = i[c].Operand as MethodReference;
-
- // TODO: Throw an error
- // This usually happens when the same XAML resource was added twice for some weird reason
- // We currently support it for dual-named default theme resources
if (op != null
&& op.Name == TrampolineName)
{
- foundXamlLoader = true;
- break;
+ throw new InvalidProgramException("Same XAML file was loaded twice." +
+ "Make sure there is no x:Class duplicates no files were added to the AvaloniaResource msbuild items group twice.");
}
if (op != null
&& op.Name == "Load"
@@ -417,12 +451,12 @@ namespace Avalonia.Build.Tasks
}
- if (buildName != null || classTypeDefinition != null)
+ if (document.BuildMethod != null || classTypeDefinition != null)
{
- var compiledBuildMethod = buildName == null ?
+ var compiledBuildMethod = document.BuildMethod == null ?
null :
typeSystem.GetTypeReference(builder).Resolve()
- .Methods.First(m => m.Name == buildName);
+ .Methods.First(m => m.Name == document.BuildMethod?.Name);
var parameterlessConstructor = compiledBuildMethod != null ?
null :
classTypeDefinition.GetConstructors().FirstOrDefault(c =>
@@ -459,22 +493,12 @@ namespace Avalonia.Build.Tasks
lineNumber = xe.LineNumber;
linePosition = xe.LinePosition;
}
- engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", "XAMLIL", res.FilePath,
- lineNumber, linePosition, lineNumber, linePosition,
- e.Message, "", "Avalonia"));
+ engine.LogError(BuildEngineErrorCode.EmitError, res.FilePath, e, lineNumber, linePosition);
return false;
}
res.Remove();
}
- foreach (var (populateMethod, resourceFilePath) in populateMethodsToTransform)
- {
- if (!TransformXamlIncludes(engine, typeSystem, populateMethod, resourceFilePath, createRootServiceProviderMethod))
- {
- return false;
- }
- }
-
// Technically that's a hack, but it fixes corert incompatibility caused by deterministic builds
int dupeCounter = 1;
foreach (var grp in typeDef.NestedTypes.GroupBy(x => x.Name))
@@ -551,116 +575,5 @@ namespace Avalonia.Build.Tasks
return true;
}
-
- private static bool TransformXamlIncludes(
- IBuildEngine engine, CecilTypeSystem typeSystem,
- MethodDefinition populateMethod, string resourceFilePath,
- MethodReference createRootServiceProviderMethod)
- {
- var asm = typeSystem.TargetAssemblyDefinition;
- foreach (var instruction in populateMethod.Body.Instructions.ToArray())
- {
- const string resolveStyleIncludeName = "ResolveStyleInclude";
- const string resolveResourceInclude = "ResolveResourceInclude";
- if (instruction.OpCode == OpCodes.Call
- && instruction.Operand is MethodReference
- {
- Name: resolveStyleIncludeName or resolveResourceInclude,
- DeclaringType: { Name: "XamlIlRuntimeHelpers" }
- })
- {
- int lineNumber = 0, linePosition = 0;
- bool instructionsModified = false;
- try
- {
- var assetSource = (string)instruction.Previous.Previous.Previous.Operand;
- lineNumber = GetConstValue(instruction.Previous.Previous);
- linePosition = GetConstValue(instruction.Previous);
-
- var index = populateMethod.Body.Instructions.IndexOf(instruction);
-
- assetSource = assetSource.Replace("avares://", "");
-
- var assemblyNameSeparator = assetSource.IndexOf('/');
- var fileNameSeparator = assetSource.LastIndexOf('/');
- if (assemblyNameSeparator < 0 || fileNameSeparator < 0)
- {
- throw new InvalidProgramException(
- $"Invalid asset source \"{assetSource}\". It must contain assembly name and relative path.");
- }
-
- var assetAssemblyName = assetSource.Substring(0, assemblyNameSeparator);
- var assetAssembly = typeSystem.FindAssembly(assetAssemblyName)
- ?? throw new InvalidProgramException(
- $"Unable to resolve assembly \"{assetAssemblyName}\"");
-
- var fileName = Path.GetFileNameWithoutExtension(assetSource.Replace('/', '.'));
- if (assetAssembly.FindType(fileName) is { } type
- && type.FindConstructor() is { } ctor)
- {
- var ctorMethod =
- asm.MainModule.ImportReference(typeSystem.GetMethodReference(ctor));
- instructionsModified = true;
- populateMethod.Body.Instructions[index - 3] = Instruction.Create(OpCodes.Nop);
- populateMethod.Body.Instructions[index - 2] = Instruction.Create(OpCodes.Nop);
- populateMethod.Body.Instructions[index - 1] = Instruction.Create(OpCodes.Nop);
- populateMethod.Body.Instructions[index] = Instruction.Create(OpCodes.Newobj, ctorMethod);
- }
- else
- {
- var resources = assetAssembly.FindType("CompiledAvaloniaXaml.!AvaloniaResources")
- ?? throw new InvalidOperationException(
- $"Unable to resolve \"!AvaloniaResources\" type on \"{assetAssemblyName}\" assembly");
-
- var relativeName = "Build:" + assetSource.Substring(assemblyNameSeparator);
- var buildMethod = resources.FindMethod(m => m.Name == relativeName)
- ?? throw new InvalidOperationException(
- $"Unable to resolve build method \"{relativeName}\" resource on the \"{assetAssemblyName}\" assembly");
-
- var methodReference = asm.MainModule.ImportReference(typeSystem.GetMethodReference(buildMethod));
- instructionsModified = true;
- populateMethod.Body.Instructions[index - 3] = Instruction.Create(OpCodes.Nop);
- populateMethod.Body.Instructions[index - 2] = Instruction.Create(OpCodes.Nop);
- populateMethod.Body.Instructions[index - 1] =
- Instruction.Create(OpCodes.Call, createRootServiceProviderMethod);
- populateMethod.Body.Instructions[index] = Instruction.Create(OpCodes.Call, methodReference);
- }
- }
- catch (Exception e)
- {
- if (instructionsModified)
- {
- engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", "XAMLIL", resourceFilePath,
- lineNumber, linePosition, lineNumber, linePosition,
- e.Message, "", "Avalonia"));
- return false;
- }
- else
- {
- engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", "XAMLIL",
- resourceFilePath,
- lineNumber, linePosition, lineNumber, linePosition,
- e.Message, "", "Avalonia"));
- }
- }
-
- static int GetConstValue(Instruction instruction)
- {
- if (instruction.OpCode is { Code : >= Code.Ldc_I4_0 and <= Code.Ldc_I4_8 })
- {
- return instruction.OpCode.Code - Code.Ldc_I4_0;
- }
- if (instruction.Operand is not null)
- {
- return Convert.ToInt32(instruction.Operand);
- }
-
- return 0;
- }
- }
- }
-
- return true;
- }
}
}
diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs
index 3ba99d8a67..65227a826a 100644
--- a/src/Avalonia.Controls/Expander.cs
+++ b/src/Avalonia.Controls/Expander.cs
@@ -1,7 +1,11 @@
+using System;
using System.Threading;
using Avalonia.Animation;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
namespace Avalonia.Controls
{
@@ -37,12 +41,24 @@ namespace Avalonia.Controls
[PseudoClasses(":expanded", ":up", ":down", ":left", ":right")]
public class Expander : HeaderedContentControl
{
+ ///
+ /// Defines the property.
+ ///
public static readonly StyledProperty ContentTransitionProperty =
- AvaloniaProperty.Register(nameof(ContentTransition));
+ AvaloniaProperty.Register(
+ nameof(ContentTransition));
+ ///
+ /// Defines the property.
+ ///
public static readonly StyledProperty ExpandDirectionProperty =
- AvaloniaProperty.Register(nameof(ExpandDirection), ExpandDirection.Down);
+ AvaloniaProperty.Register(
+ nameof(ExpandDirection),
+ ExpandDirection.Down);
+ ///
+ /// Defines the property.
+ ///
public static readonly DirectProperty IsExpandedProperty =
AvaloniaProperty.RegisterDirect(
nameof(IsExpanded),
@@ -50,47 +66,206 @@ namespace Avalonia.Controls
(o, v) => o.IsExpanded = v,
defaultBindingMode: Data.BindingMode.TwoWay);
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent CollapsedEvent =
+ RoutedEvent.Register(
+ nameof(Collapsed),
+ RoutingStrategies.Bubble);
+
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent CollapsingEvent =
+ RoutedEvent.Register(
+ nameof(Collapsing),
+ RoutingStrategies.Bubble);
+
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent ExpandedEvent =
+ RoutedEvent.Register(
+ nameof(Expanded),
+ RoutingStrategies.Bubble);
+
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent ExpandingEvent =
+ RoutedEvent.Register(
+ nameof(Expanding),
+ RoutingStrategies.Bubble);
+
+ private bool _ignorePropertyChanged = false;
private bool _isExpanded;
private CancellationTokenSource? _lastTransitionCts;
- static Expander()
- {
- IsExpandedProperty.Changed.AddClassHandler((x, e) => x.OnIsExpandedChanged(e));
- }
-
+ ///
+ /// Initializes a new instance of the class.
+ ///
public Expander()
{
- UpdatePseudoClasses(ExpandDirection);
+ UpdatePseudoClasses();
}
+ ///
+ /// Gets or sets the transition used when expanding or collapsing the content.
+ ///
public IPageTransition? ContentTransition
{
get => GetValue(ContentTransitionProperty);
set => SetValue(ContentTransitionProperty, value);
}
+ ///
+ /// Gets or sets the direction in which the opens.
+ ///
public ExpandDirection ExpandDirection
{
get => GetValue(ExpandDirectionProperty);
set => SetValue(ExpandDirectionProperty, value);
}
+ ///
+ /// Gets or sets a value indicating whether the
+ /// content area is open and visible.
+ ///
public bool IsExpanded
{
- get { return _isExpanded; }
- set
- {
- SetAndRaise(IsExpandedProperty, ref _isExpanded, value);
- PseudoClasses.Set(":expanded", value);
+ get => _isExpanded;
+ set
+ {
+ // It is important here that IsExpanded is a direct property so events can be invoked
+ // BEFORE the property system gets notified of updated values. This is because events
+ // may be canceled by external code.
+ if (_isExpanded != value)
+ {
+ RoutedEventArgs eventArgs;
+
+ if (value)
+ {
+ eventArgs = new RoutedEventArgs(ExpandingEvent, this);
+ OnExpanding(eventArgs);
+ }
+ else
+ {
+ eventArgs = new RoutedEventArgs(CollapsingEvent, this);
+ OnCollapsing(eventArgs);
+ }
+
+ if (eventArgs.Handled)
+ {
+ // If the event was externally handled (canceled) we must still notify the value has changed.
+ // This property changed notification will update any external code observing this property that itself may have set the new value.
+ // We are essentially reverted any external state change along with ignoring the IsExpanded property set.
+ // Remember IsExpanded is usually controlled by a ToggleButton in the control theme.
+ _ignorePropertyChanged = true;
+
+ RaisePropertyChanged(
+ IsExpandedProperty,
+ oldValue: value,
+ newValue: _isExpanded,
+ BindingPriority.LocalValue,
+ isEffectiveValue: true);
+
+ _ignorePropertyChanged = false;
+ }
+ else
+ {
+ SetAndRaise(IsExpandedProperty, ref _isExpanded, value);
+ }
+ }
}
}
- protected virtual async void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e)
+ ///
+ /// Occurs after the content area has closed and only the header is visible.
+ ///
+ public event EventHandler? Collapsed
+ {
+ add => AddHandler(CollapsedEvent, value);
+ remove => RemoveHandler(CollapsedEvent, value);
+ }
+
+ ///
+ /// Occurs as the content area is closing.
+ ///
+ ///
+ /// The event args property may be set to true to cancel the event
+ /// and keep the control open (expanded).
+ ///
+ public event EventHandler? Collapsing
+ {
+ add => AddHandler(CollapsingEvent, value);
+ remove => RemoveHandler(CollapsingEvent, value);
+ }
+
+ ///
+ /// Occurs after the has opened to display both its header and content.
+ ///
+ public event EventHandler? Expanded
+ {
+ add => AddHandler(ExpandedEvent, value);
+ remove => RemoveHandler(ExpandedEvent, value);
+ }
+
+ ///
+ /// Occurs as the content area is opening.
+ ///
+ ///
+ /// The event args property may be set to true to cancel the event
+ /// and keep the control closed (collapsed).
+ ///
+ public event EventHandler? Expanding
+ {
+ add => AddHandler(ExpandingEvent, value);
+ remove => RemoveHandler(ExpandingEvent, value);
+ }
+
+ ///
+ /// Invoked just before the event.
+ ///
+ protected virtual void OnCollapsed(RoutedEventArgs eventArgs)
+ {
+ RaiseEvent(eventArgs);
+ }
+
+ ///
+ /// Invoked just before the event.
+ ///
+ protected virtual void OnCollapsing(RoutedEventArgs eventArgs)
+ {
+ RaiseEvent(eventArgs);
+ }
+
+ ///
+ /// Invoked just before the event.
+ ///
+ protected virtual void OnExpanded(RoutedEventArgs eventArgs)
+ {
+ RaiseEvent(eventArgs);
+ }
+
+ ///
+ /// Invoked just before the event.
+ ///
+ protected virtual void OnExpanding(RoutedEventArgs eventArgs)
+ {
+ RaiseEvent(eventArgs);
+ }
+
+ ///
+ /// Starts the content transition (if set) and invokes the
+ /// and events when completed.
+ ///
+ private async void StartContentTransition()
{
if (Content != null && ContentTransition != null && Presenter is Visual visualContent)
{
bool forward = ExpandDirection == ExpandDirection.Left ||
- ExpandDirection == ExpandDirection.Up;
+ ExpandDirection == ExpandDirection.Up;
_lastTransitionCts?.Cancel();
_lastTransitionCts = new CancellationTokenSource();
@@ -104,24 +279,58 @@ namespace Avalonia.Controls
await ContentTransition.Start(visualContent, null, forward, _lastTransitionCts.Token);
}
}
+
+ // Expanded/Collapsed events are invoked asynchronously to ensure other events,
+ // such as Click, have time to complete first.
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (IsExpanded)
+ {
+ OnExpanded(new RoutedEventArgs(ExpandedEvent, this));
+ }
+ else
+ {
+ OnCollapsed(new RoutedEventArgs(CollapsedEvent, this));
+ }
+ });
}
+ ///
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
+ if (_ignorePropertyChanged)
+ {
+ return;
+ }
+
if (change.Property == ExpandDirectionProperty)
{
- UpdatePseudoClasses(change.GetNewValue());
+ UpdatePseudoClasses();
+ }
+ else if (change.Property == IsExpandedProperty)
+ {
+ // Expanded/Collapsed will be raised once transitions are complete
+ StartContentTransition();
+
+ UpdatePseudoClasses();
}
}
- private void UpdatePseudoClasses(ExpandDirection d)
+ ///
+ /// Updates the visual state of the control by applying latest PseudoClasses.
+ ///
+ private void UpdatePseudoClasses()
{
- PseudoClasses.Set(":up", d == ExpandDirection.Up);
- PseudoClasses.Set(":down", d == ExpandDirection.Down);
- PseudoClasses.Set(":left", d == ExpandDirection.Left);
- PseudoClasses.Set(":right", d == ExpandDirection.Right);
+ var expandDirection = ExpandDirection;
+
+ PseudoClasses.Set(":up", expandDirection == ExpandDirection.Up);
+ PseudoClasses.Set(":down", expandDirection == ExpandDirection.Down);
+ PseudoClasses.Set(":left", expandDirection == ExpandDirection.Left);
+ PseudoClasses.Set(":right", expandDirection == ExpandDirection.Right);
+
+ PseudoClasses.Set(":expanded", IsExpanded);
}
}
}
diff --git a/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs b/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs
index 0f257224dd..8590974462 100644
--- a/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs
+++ b/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs
@@ -18,7 +18,7 @@ namespace Avalonia.Controls
}
}
- base.OnKeyDown(e);
+ base.OnKeyDown(e);
}
}
}
diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs
index 9a901f909a..b4cfffcdca 100644
--- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs
+++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs
@@ -38,10 +38,9 @@ namespace Avalonia.DesignerSupport
var useCompiledBindings = localAsm?.GetCustomAttributes()
.FirstOrDefault(a => a.Key == "AvaloniaUseCompiledBindingsByDefault")?.Value;
- var loaded = loader.Load(stream, new RuntimeXamlLoaderConfiguration
+ var loaded = loader.Load(new RuntimeXamlLoaderDocument(baseUri, stream), new RuntimeXamlLoaderConfiguration
{
LocalAssembly = localAsm,
- BaseUri = baseUri,
DesignMode = true,
UseCompiledBindingsByDefault = bool.TryParse(useCompiledBindings, out var parsedValue ) && parsedValue
});
diff --git a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
index 3cf819c4b8..37e9b71185 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
@@ -103,17 +103,18 @@
RenderTransformOrigin="50%,50%"
Stretch="None"
Stroke="{DynamicResource ExpanderChevronForeground}"
- StrokeThickness="1" />
-
-
-
+ StrokeThickness="1">
+
+
+
+
-
-
-
-";
-
- using (StyledWindow(assets: ("test:style.xaml", styleXaml)))
- {
- var xaml = @"
+"),
+ new RuntimeXamlLoaderDocument(@"
-
+
-";
-
- var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+")
+ };
+
+ using (StyledWindow())
+ {
+ var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+ var window = Assert.IsType(compiled[1]);
var border = window.FindControl("border");
var brush = (ISolidColorBrush)border.Background;
@@ -284,13 +286,14 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
[Fact]
public void DynamicResource_Can_Be_Assigned_To_Property_In_ControlTemplate_In_Styles_File()
{
- var styleXaml = @"
+ var documents = new[]
+ {
+ new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style.xaml"), @"
#ff506070
-
-";
-
- using (StyledWindow(assets: ("test:style.xaml", styleXaml)))
- {
- var xaml = @"
+"),
+ new RuntimeXamlLoaderDocument(@"
-
+
-";
-
- var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+")
+ };
+
+ using (StyledWindow())
+ {
+ var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+ var window = Assert.IsType(compiled[1]);
var button = window.FindControl")
+ };
+
+ using (StyledWindow())
+ {
+ var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+ var window = Assert.IsType(compiled[2]);
var border = window.FindControl("border");
var borderBrush = (ISolidColorBrush)border.Background;
@@ -593,33 +599,35 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
[Fact]
public void DynamicResource_Can_Be_Found_In_Nested_Style_File()
{
- var style1Xaml = @"
+ var documents = new[]
+ {
+ new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style1.xaml"), @"
-
-";
- var style2Xaml = @"
+
+"),
+ new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style2.xaml"), @"
";
- using (StyledWindow(
- ("test:style1.xaml", style1Xaml),
- ("test:style2.xaml", style2Xaml)))
- {
- var xaml = @"
+"),
+ new RuntimeXamlLoaderDocument(@"
-
+
-";
-
- var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+")
+ };
+
+ using (StyledWindow())
+ {
+ var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+ var window = Assert.IsType(compiled[2]);
var border = window.FindControl("border");
var borderBrush = (ISolidColorBrush)border.Background;
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs
index d8ecba36f5..ac4000b4c6 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs
@@ -14,29 +14,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
[Fact]
public void ResourceInclude_Loads_ResourceDictionary()
{
- var includeXaml = @"
+ var documents = new[]
+ {
+ new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resource.xaml"), @"
#ff506070
-
-";
- using (StartWithResources(("test:include.xaml", includeXaml)))
- {
- var xaml = @"
+"),
+ new RuntimeXamlLoaderDocument(@"
-
+
-";
+")
+ };
- var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
+ using (StartWithResources())
+ {
+ var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+ var userControl = Assert.IsType(compiled[1]);
var border = userControl.FindControl("border");
var brush = (ISolidColorBrush)border.Background;
@@ -47,31 +50,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
[Fact]
public void Missing_ResourceKey_In_ResourceInclude_Does_Not_Cause_StackOverflow()
{
- var styleXaml = @"
+ var app = Application.Current;
+ var documents = new[]
+ {
+ new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resource.xaml"), @"
-";
-
- using (StartWithResources(("test:style.xaml", styleXaml)))
- {
- var xaml = @"
+"),
+ new RuntimeXamlLoaderDocument(app, @"
-
+
-";
-
- var app = Application.Current;
+")
+ };
+ using (StartWithResources())
+ {
try
{
- AvaloniaRuntimeXamlLoader.Load(xaml, null, app);
+ AvaloniaRuntimeXamlLoader.LoadGroup(documents);
}
catch (KeyNotFoundException)
{
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs
index 340eac0d4f..b9afd5fdba 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs
@@ -238,7 +238,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
[Fact]
public void StaticResource_Can_Be_Assigned_To_Setter_In_Styles_File()
{
- var styleXaml = @"
+ var documents = new[]
+ {
+ new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style.xaml"), @"
@@ -248,20 +250,21 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
-";
-
- using (StyledWindow(assets: ("test:style.xaml", styleXaml)))
- {
- var xaml = @"
+"),
+ new RuntimeXamlLoaderDocument(@"
-
+
-";
+")
+ };
- var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+ using (StyledWindow())
+ {
+ var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+ var window = Assert.IsType(compiled[1]);
var border = window.FindControl("border");
var brush = (ISolidColorBrush)border.Background;
@@ -311,7 +314,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
[Fact]
public void StaticResource_Can_Be_Assigned_To_Property_In_ControlTemplate_In_Styles_File()
{
- var styleXaml = @"
+ var documents = new[]
+ {
+ new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style.xaml"), @"
@@ -325,20 +330,21 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
-";
-
- using (StyledWindow(assets: ("test:style.xaml", styleXaml)))
- {
- var xaml = @"
+"),
+ new RuntimeXamlLoaderDocument(@"
-
+
-";
-
- var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+")
+ };
+
+ using (StyledWindow())
+ {
+ var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+ var window = Assert.IsType(compiled[1]);
var button = window.FindControl")
+ };
- var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+ var loaded = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+ var window = Assert.IsType(loaded[1]);
var button = window.FindControl("button");
var brush = Assert.IsType(button.Background);
@@ -276,7 +279,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
private IDisposable StyledWindow(params (string, string)[] assets)
{
var services = TestServices.StyledWindow.With(
- assetLoader: new MockAssetLoader(assets),
theme: () => new Styles
{
WindowStyle(),
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/Style1.xaml b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/Style1.xaml
index 1f195c4605..fefd2021e2 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/Style1.xaml
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/Style1.xaml
@@ -1,6 +1,5 @@
"),
+ new RuntimeXamlLoaderDocument(@"
+
+
+
+
+")
+ };
+
+ var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+ var style = Assert.IsType"),
+ new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Subfolder/Folder/Root.xaml"), @"
+
+
+
+
+")
+ };
+
+ var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+ var style = Assert.IsType"),
+ new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Root.xaml"), @"
+
+
+
+
+")
+ };
+
+ var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+ var style = Assert.IsType"),
+ new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Root.xaml"), @"
+
+
+
+
+")
+ };
+
+ var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+ var style = Assert.IsType"),
+ new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Root.xaml"), @"
+
+
+
+
+")
+ };
+
+ var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+ var style = Assert.IsType"),
+ new RuntimeXamlLoaderDocument(@"
+
+
+
+
+")
+ };
+
+ var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+ var style = Assert.IsType"),
+ new RuntimeXamlLoaderDocument(@"
+
+
+
+
+")
+ };
+
+
+ try
+ {
+ _ = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+ }
+ catch (KeyNotFoundException)
+ {
+
+ }
+ }
+
+ [Fact]
+ public void StyleInclude_Should_Be_Replaced_With_Direct_Call()
+ {
+ var control = (ContentControl)AvaloniaRuntimeXamlLoader.Load(@"
+
+
+
+
+
+");
+ Assert.IsType(control.Styles[0]);
+ Assert.IsType(control.Styles[1]);
+ }
+}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
index 339609cb4f..70a5295008 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
@@ -109,45 +109,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
- [Fact]
- public void StyleInclude_Is_Built()
- {
- using (UnitTestApplication.Start(TestServices.StyledWindow
- .With(theme: () => new Styles())))
- {
- var xaml = @"
-
-
-
-
-";
-
- var window = AvaloniaRuntimeXamlLoader.Parse(xaml);
-
- Assert.IsType