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(@" - +