From 70440517ca2dda3f46d87d94f9e7f326d79299ca Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 20 Jun 2018 23:53:54 +0200 Subject: [PATCH 01/10] Initial implementation of StringFormat. XAML tests failing because of https://github.com/cwensley/Portable.Xaml/pull/106 --- .../Data/Converters/StringFormatConverter.cs | 30 ++++ .../MarkupExtensions/BindingExtension.cs | 3 + src/Markup/Avalonia.Markup/Data/Binding.cs | 21 ++- .../Data/BindingTests_Converters.cs | 146 ++++++++++++++++++ .../Xaml/BindingTests.cs | 22 +++ 5 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Base/Data/Converters/StringFormatConverter.cs create mode 100644 tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs diff --git a/src/Avalonia.Base/Data/Converters/StringFormatConverter.cs b/src/Avalonia.Base/Data/Converters/StringFormatConverter.cs new file mode 100644 index 0000000000..67ecacd1c7 --- /dev/null +++ b/src/Avalonia.Base/Data/Converters/StringFormatConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; + +namespace Avalonia.Data.Converters +{ + public class StringFormatConverter : IValueConverter + { + public StringFormatConverter(string format, IValueConverter inner) + { + Contract.Requires(format != null); + + Format = format; + Inner = inner; + } + + public IValueConverter Inner { get; } + public string Format { get; } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + value = Inner?.Convert(value, targetType, parameter, culture) ?? value; + return string.Format(Format, value, culture); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException("Two way bindings are not supported with a string format"); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 98203deebe..4349b5e82e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -45,6 +45,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions Path = pathInfo.Path, Priority = Priority, Source = Source, + StringFormat = StringFormat, RelativeSource = pathInfo.RelativeSource ?? RelativeSource, DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider)) }; @@ -226,6 +227,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public object Source { get; set; } + public string StringFormat { get; set; } + public RelativeSource RelativeSource { get; set; } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 96fc2986e8..a39dbe72cd 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -84,6 +84,11 @@ namespace Avalonia.Data /// public object Source { get; set; } + /// + /// Gets or sets the string format. + /// + public string StringFormat { get; set; } + public WeakReference DefaultAnchor { get; set; } /// @@ -158,11 +163,23 @@ namespace Avalonia.Data fallback = null; } + var converter = Converter; + var targetType = targetProperty?.PropertyType ?? typeof(object); + + // We only respect `StringFormat` if the type of the property we're assigning to will + // accept a string. Note that this is slightly different to WPF in that WPF only applies + // `StringFormat` for target type `string` (not `object`). + if (!string.IsNullOrWhiteSpace(StringFormat) && + (targetType == typeof(string) || targetType == typeof(object))) + { + converter = new StringFormatConverter(StringFormat, converter); + } + var subject = new BindingExpression( observer, - targetProperty?.PropertyType ?? typeof(object), + targetType, fallback, - Converter ?? DefaultValueConverter.Instance, + converter ?? DefaultValueConverter.Instance, ConverterParameter, Priority); diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs new file mode 100644 index 0000000000..814618d764 --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs @@ -0,0 +1,146 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Data.Core; +using Xunit; + +namespace Avalonia.Markup.UnitTests.Data +{ + public class BindingTests_Converters + { + [Fact] + public void Converter_Should_Be_Used() + { + var textBlock = new TextBlock + { + DataContext = new Class1(), + }; + + var target = new Binding(nameof(Class1.Foo)) + { + Converter = StringConverters.NullOrEmpty, + }; + + var expressionObserver = (BindingExpression)target.Initiate( + textBlock, + TextBlock.TextProperty).Observable; + + Assert.Same(StringConverters.NullOrEmpty, expressionObserver.Converter); + } + + public class When_Binding_To_String + { + [Fact] + public void StringFormatConverter_Should_Used_When_Binding_Has_StringFormat() + { + var textBlock = new TextBlock + { + DataContext = new Class1(), + }; + + var target = new Binding(nameof(Class1.Foo)) + { + StringFormat = "Hello {0}", + }; + + var expressionObserver = (BindingExpression)target.Initiate( + textBlock, + TextBlock.TextProperty).Observable; + + Assert.IsType(expressionObserver.Converter); + } + } + + public class When_Binding_To_Object + { + [Fact] + public void StringFormatConverter_Should_Used_When_Binding_Has_StringFormat() + { + var textBlock = new TextBlock + { + DataContext = new Class1(), + }; + + var target = new Binding(nameof(Class1.Foo)) + { + StringFormat = "Hello {0}", + }; + + var expressionObserver = (BindingExpression)target.Initiate( + textBlock, + TextBlock.TagProperty).Observable; + + Assert.IsType(expressionObserver.Converter); + } + } + + public class When_Binding_To_Non_String_Or_Object + { + [Fact] + public void StringFormatConverter_Should_Not_Be_Used_When_Binding_Has_StringFormat() + { + var textBlock = new TextBlock + { + DataContext = new Class1(), + }; + + var target = new Binding(nameof(Class1.Foo)) + { + StringFormat = "Hello {0}", + }; + + var expressionObserver = (BindingExpression)target.Initiate( + textBlock, + TextBlock.MarginProperty).Observable; + + Assert.Same(DefaultValueConverter.Instance, expressionObserver.Converter); + } + } + + [Fact] + public void StringFormat_Should_Be_Applied() + { + var textBlock = new TextBlock + { + DataContext = new Class1(), + }; + + var target = new Binding(nameof(Class1.Foo)) + { + StringFormat = "Hello {0}", + }; + + textBlock.Bind(TextBlock.TextProperty, target); + + Assert.Equal("Hello foo", textBlock.Text); + } + + [Fact] + public void StringFormat_Should_Be_Applied_After_Converter() + { + var textBlock = new TextBlock + { + DataContext = new Class1(), + }; + + var target = new Binding(nameof(Class1.Foo)) + { + Converter = StringConverters.NotNullOrEmpty, + StringFormat = "Hello {0}", + }; + + textBlock.Bind(TextBlock.TextProperty, target); + + Assert.Equal("Hello True", textBlock.Text); + } + + private class Class1 + { + public string Foo { get; set; } = "foo"; + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index 568c6482f5..14952bca09 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -281,5 +281,27 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(5.6, AttachedPropertyOwner.GetDouble(textBlock)); } } + + [Fact] + public void Binding_To_TextBlock_Text_With_StringConverter_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + textBlock.DataContext = new { Foo = "world" }; + window.ApplyTemplate(); + + Assert.Equal("Hello world", textBlock.Text); + } + } } } \ No newline at end of file From 740b6073f0de1f14da38b620302fb5797130077e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Jun 2018 16:42:24 +0200 Subject: [PATCH 02/10] Escape braces in format string. --- tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index 14952bca09..18cb3c74fc 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -291,7 +291,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml - + "; var loader = new AvaloniaXamlLoader(); var window = (Window)loader.Load(xaml); From 6a14747eb658d59928a8ab3f9c294e29bfcfb949 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Jun 2018 18:24:18 +0200 Subject: [PATCH 03/10] StringFormatConverter -> StringFormatValueConverter --- ...StringFormatConverter.cs => StringFormatValueConverter.cs} | 4 ++-- src/Markup/Avalonia.Markup/Data/Binding.cs | 2 +- .../Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/Avalonia.Base/Data/Converters/{StringFormatConverter.cs => StringFormatValueConverter.cs} (85%) diff --git a/src/Avalonia.Base/Data/Converters/StringFormatConverter.cs b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs similarity index 85% rename from src/Avalonia.Base/Data/Converters/StringFormatConverter.cs rename to src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs index 67ecacd1c7..3703929356 100644 --- a/src/Avalonia.Base/Data/Converters/StringFormatConverter.cs +++ b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs @@ -3,9 +3,9 @@ using System.Globalization; namespace Avalonia.Data.Converters { - public class StringFormatConverter : IValueConverter + public class StringFormatValueConverter : IValueConverter { - public StringFormatConverter(string format, IValueConverter inner) + public StringFormatValueConverter(string format, IValueConverter inner) { Contract.Requires(format != null); diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index a39dbe72cd..62cff4d768 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -172,7 +172,7 @@ namespace Avalonia.Data if (!string.IsNullOrWhiteSpace(StringFormat) && (targetType == typeof(string) || targetType == typeof(object))) { - converter = new StringFormatConverter(StringFormat, converter); + converter = new StringFormatValueConverter(StringFormat, converter); } var subject = new BindingExpression( diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs index 814618d764..e98f9f895c 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs @@ -51,7 +51,7 @@ namespace Avalonia.Markup.UnitTests.Data textBlock, TextBlock.TextProperty).Observable; - Assert.IsType(expressionObserver.Converter); + Assert.IsType(expressionObserver.Converter); } } @@ -74,7 +74,7 @@ namespace Avalonia.Markup.UnitTests.Data textBlock, TextBlock.TagProperty).Observable; - Assert.IsType(expressionObserver.Converter); + Assert.IsType(expressionObserver.Converter); } } From 09454783577847b58b780bdd695a3d08a80e5150 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Jun 2018 18:38:39 +0200 Subject: [PATCH 04/10] Fix grammar. --- .../Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs index e98f9f895c..123aadfda5 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs @@ -35,7 +35,7 @@ namespace Avalonia.Markup.UnitTests.Data public class When_Binding_To_String { [Fact] - public void StringFormatConverter_Should_Used_When_Binding_Has_StringFormat() + public void StringFormatConverter_Should_Be_Used_When_Binding_Has_StringFormat() { var textBlock = new TextBlock { @@ -58,7 +58,7 @@ namespace Avalonia.Markup.UnitTests.Data public class When_Binding_To_Object { [Fact] - public void StringFormatConverter_Should_Used_When_Binding_Has_StringFormat() + public void StringFormatConverter_Should_Be_Used_When_Binding_Has_StringFormat() { var textBlock = new TextBlock { From 63a628731e533d5aa41c367b9cfae19c4db83944 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Jun 2018 18:56:37 +0200 Subject: [PATCH 05/10] Added XML docs for StringFormatValueConverter. --- .../Converters/StringFormatValueConverter.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs index 3703929356..643f26a3c1 100644 --- a/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs @@ -3,8 +3,18 @@ using System.Globalization; namespace Avalonia.Data.Converters { + /// + /// A value converter which calls + /// public class StringFormatValueConverter : IValueConverter { + /// + /// Initializes a new instance of the class. + /// + /// The format string. + /// + /// An optional inner converter to be called before the format takes place. + /// public StringFormatValueConverter(string format, IValueConverter inner) { Contract.Requires(format != null); @@ -13,15 +23,24 @@ namespace Avalonia.Data.Converters Inner = inner; } + /// + /// Gets an inner value converter which will be called before the string format takes place. + /// public IValueConverter Inner { get; } + + /// + /// Gets the format string. + /// public string Format { get; } + /// public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { value = Inner?.Convert(value, targetType, parameter, culture) ?? value; return string.Format(Format, value, culture); } + /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotSupportedException("Two way bindings are not supported with a string format"); From d0bc9f19a3067f0fbf0891f2c45be731baecb58f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 22 Jun 2018 09:56:27 +0200 Subject: [PATCH 06/10] Use correct parameter order! --- src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs index 643f26a3c1..fc695762b8 100644 --- a/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs @@ -37,7 +37,7 @@ namespace Avalonia.Data.Converters public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { value = Inner?.Convert(value, targetType, parameter, culture) ?? value; - return string.Format(Format, value, culture); + return string.Format(culture, Format, value); } /// From c5a21590211e45e345d052340f3f9cde8cfbc5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 13 Aug 2018 14:48:09 +0200 Subject: [PATCH 07/10] Add support for Direct2D1 for Avalonia.DotNetCoreRuntime --- src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj index f80462e958..0aed0a9717 100644 --- a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj +++ b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj @@ -10,6 +10,7 @@ + From 9c6e69914dbc8f6003e319d0d2c2f847d5fd089c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Aug 2018 18:06:42 +0100 Subject: [PATCH 08/10] fixes an occasional crash with popup root "SnapInsideScreenEdges" --- src/Avalonia.Controls/Primitives/PopupRoot.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 457a7bd4b4..996eb6204d 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -83,9 +83,7 @@ namespace Avalonia.Controls.Primitives /// public void SnapInsideScreenEdges() { - var window = this.GetSelfAndLogicalAncestors().OfType().First(); - - var screen = window.Screens.ScreenFromPoint(Position); + var screen = Application.Current.MainWindow.Screens.ScreenFromPoint(Position); var screenX = Position.X + Bounds.Width - screen.Bounds.X; var screenY = Position.Y + Bounds.Height - screen.Bounds.Y; From 7d28d1b2abfecfd6e6ca8e1f44739a256c4fe09d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Aug 2018 18:21:02 +0100 Subject: [PATCH 09/10] fix failing unit test. --- src/Avalonia.Controls/Primitives/PopupRoot.cs | 23 +++++++++++-------- .../ContextMenuTests.cs | 5 +++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 996eb6204d..0ae4be5550 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -83,19 +83,22 @@ namespace Avalonia.Controls.Primitives /// public void SnapInsideScreenEdges() { - var screen = Application.Current.MainWindow.Screens.ScreenFromPoint(Position); + var screen = Application.Current.MainWindow?.Screens.ScreenFromPoint(Position); - var screenX = Position.X + Bounds.Width - screen.Bounds.X; - var screenY = Position.Y + Bounds.Height - screen.Bounds.Y; - - if (screenX > screen.Bounds.Width) + if (screen != null) { - Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width)); - } + var screenX = Position.X + Bounds.Width - screen.Bounds.X; + var screenY = Position.Y + Bounds.Height - screen.Bounds.Y; - if (screenY > screen.Bounds.Height) - { - Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height)); + if (screenX > screen.Bounds.Width) + { + Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width)); + } + + if (screenY > screen.Bounds.Height) + { + Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height)); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 94d5caa720..66f0cc3a40 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -64,7 +64,10 @@ namespace Avalonia.Controls.UnitTests { ContextMenu = sut }; - new Window { Content = target }; + + var window = new Window { Content = target }; + + Avalonia.Application.Current.MainWindow = window; target.RaiseEvent(new PointerReleasedEventArgs { From dbe8f748c963f201d398da7bd9b4f44c97b5d046 Mon Sep 17 00:00:00 2001 From: WojciechKrysiak Date: Mon, 13 Aug 2018 20:46:22 +0200 Subject: [PATCH 10/10] Single menu context menus working. (#1808) --- src/OSX/Avalonia.MonoMac/PopupImpl.cs | 14 +++++++++++++- src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs | 16 ++++++++-------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/OSX/Avalonia.MonoMac/PopupImpl.cs b/src/OSX/Avalonia.MonoMac/PopupImpl.cs index ba4b7f0eac..267e47a6a0 100644 --- a/src/OSX/Avalonia.MonoMac/PopupImpl.cs +++ b/src/OSX/Avalonia.MonoMac/PopupImpl.cs @@ -8,11 +8,23 @@ namespace Avalonia.MonoMac public PopupImpl() { UpdateStyle(); + Window.Level = NSWindowLevel.PopUpMenu; } protected override NSWindowStyle GetStyle() { return NSWindowStyle.Borderless; } + + protected override CustomWindow CreateCustomWindow() => new CustomPopupWindow(this); + + private class CustomPopupWindow : CustomWindow + { + public CustomPopupWindow(WindowBaseImpl impl) + : base(impl) + { } + + public override bool WorksWhenModal() => true; + } } -} \ No newline at end of file +} diff --git a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs index e7d82ae25e..d3053a6af1 100644 --- a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs +++ b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs @@ -19,14 +19,13 @@ namespace Avalonia.MonoMac public WindowBaseImpl() { _managedDrag = new ManagedWindowResizeDragHelper(this, _ => { }, ResizeForManagedDrag); - Window = new CustomWindow(this) - { - StyleMask = NSWindowStyle.Titled, - BackingType = NSBackingStore.Buffered, - ContentView = View, - // ReSharper disable once VirtualMemberCallInConstructor - Delegate = CreateWindowDelegate() - }; + // ReSharper disable once VirtualMemberCallInConstructor + Window = CreateCustomWindow(); + Window.StyleMask = NSWindowStyle.Titled; + Window.BackingType = NSBackingStore.Buffered; + Window.ContentView = View; + // ReSharper disable once VirtualMemberCallInConstructor + Window.Delegate = CreateWindowDelegate(); } public class CustomWindow : NSWindow @@ -57,6 +56,7 @@ namespace Avalonia.MonoMac public void SetCanBecomeKeyAndMain() => _canBecomeKeyAndMain = true; } + protected virtual CustomWindow CreateCustomWindow() => new CustomWindow(this); protected virtual NSWindowDelegate CreateWindowDelegate() => new WindowBaseDelegate(this); public class WindowBaseDelegate : NSWindowDelegate