From 7b9d32af85aa08580380ce81d0a999db727a3240 Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Sun, 9 Feb 2020 15:50:42 +0000 Subject: [PATCH 001/298] Rework system decorations --- native/Avalonia.Native/inc/avalonia-native.h | 2 +- native/Avalonia.Native/src/OSX/window.mm | 41 +++++++++++++---- samples/ControlCatalog/MainView.xaml | 11 ++++- samples/ControlCatalog/MainView.xaml.cs | 14 ++++++ src/Avalonia.Controls/Platform/IWindowImpl.cs | 2 +- src/Avalonia.Controls/Window.cs | 44 ++++++++++++++++++- .../Remote/PreviewerWindowImpl.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 2 +- src/Avalonia.Native/WindowImpl.cs | 4 +- src/Avalonia.X11/X11Window.cs | 16 ++++--- .../Interop/UnmanagedMethods.cs | 10 +++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 20 +++++---- src/iOS/Avalonia.iOS/EmbeddableImpl.cs | 2 +- 13 files changed, 137 insertions(+), 33 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 4a960d47a1..ce4a592d67 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -236,7 +236,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase { virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; virtual HRESULT SetCanResize(bool value) = 0; - virtual HRESULT SetHasDecorations(bool value) = 0; + virtual HRESULT SetHasDecorations(int value) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0; virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index b6ce172ffa..317d03162b 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -115,7 +115,6 @@ public: [NSApp activateIgnoringOtherApps:YES]; [Window setTitle:_lastTitle]; - [Window setTitleVisibility:NSWindowTitleVisible]; return S_OK; } @@ -411,7 +410,7 @@ class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, pub { private: bool _canResize = true; - bool _hasDecorations = true; + int _hasDecorations = 2; CGRect _lastUndecoratedFrame; AvnWindowState _lastWindowState; @@ -476,12 +475,12 @@ private: bool IsZoomed () { - return _hasDecorations ? [Window isZoomed] : UndecoratedIsMaximized(); + return _hasDecorations > 0 ? [Window isZoomed] : UndecoratedIsMaximized(); } void DoZoom() { - if (_hasDecorations) + if (_hasDecorations > 0) { [Window performZoom:Window]; } @@ -506,13 +505,36 @@ private: } } - virtual HRESULT SetHasDecorations(bool value) override + virtual HRESULT SetHasDecorations(int value) override { @autoreleasepool { _hasDecorations = value; UpdateStyle(); + // full + if (_hasDecorations == 2) + { + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleVisible]; + [Window setTitlebarAppearsTransparent:NO]; + [Window setTitle:_lastTitle]; + } + // border only + else if (_hasDecorations == 1) + { + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + } + // none + else + { + [Window setHasShadow:NO]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + } + return S_OK; } } @@ -523,7 +545,6 @@ private: { _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title]; [Window setTitle:_lastTitle]; - [Window setTitleVisibility:NSWindowTitleVisible]; return S_OK; } @@ -645,9 +666,11 @@ protected: virtual NSWindowStyleMask GetStyle() override { unsigned long s = NSWindowStyleMaskBorderless; - if(_hasDecorations) - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; - if(_canResize) + if(_hasDecorations == 1) + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + if(_hasDecorations == 2) + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; + if(_hasDecorations == 2 && _canResize) s = s | NSWindowStyleMaskResizable; return s; } diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index cbe2c62890..1c2653f73a 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -58,10 +58,17 @@ - + + + No Decorations + Border Only + Full Decorations + + Light Dark - + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index acb9bc5bc6..5f71b2ecd9 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -56,6 +56,20 @@ namespace ControlCatalog } }; Styles.Add(light); + + var decorations = this.Find("Decorations"); + decorations.SelectionChanged += (sender, e) => + { + Window window = (Window)VisualRoot; + window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex; + }; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + var decorations = this.Find("Decorations"); + decorations.SelectedIndex = (int)((Window)VisualRoot).SystemDecorations; } } } diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 91b895f38a..238070bbef 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -36,7 +36,7 @@ namespace Avalonia.Platform /// /// Enables or disables system window decorations (title bar, buttons, etc) /// - void SetSystemDecorations(bool enabled); + void SetSystemDecorations(SystemDecorations enabled); /// /// Sets the icon of this window. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index f66a248aaf..853347bf41 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -45,6 +45,27 @@ namespace Avalonia.Controls WidthAndHeight = 3, } + /// + /// Determines system decorations (title bar, border, etc) for a + /// + public enum SystemDecorations + { + /// + /// No decorations + /// + None = 0, + + /// + /// Window border without titlebar + /// + BorderOnly = 1, + + /// + /// Fully decorated (default) + /// + Full = 2 + } + /// /// A top-level window. /// @@ -59,9 +80,16 @@ namespace Avalonia.Controls /// /// Enables or disables system window decorations (title bar, buttons, etc) /// + [Obsolete("Use SystemDecorationsProperty instead")] public static readonly StyledProperty HasSystemDecorationsProperty = AvaloniaProperty.Register(nameof(HasSystemDecorations), true); + /// + /// Defines the property. + /// + public static readonly StyledProperty SystemDecorationsProperty = + AvaloniaProperty.Register(nameof(SystemDecorations), SystemDecorations.Full); + /// /// Enables or disables the taskbar icon /// @@ -125,7 +153,9 @@ namespace Avalonia.Controls BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue)); HasSystemDecorationsProperty.Changed.AddClassHandler( - (s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue)); + (s, e) => s.PlatformImpl?.SetSystemDecorations(((bool)e.NewValue) ? SystemDecorations.Full : SystemDecorations.None)); + SystemDecorationsProperty.Changed.AddClassHandler( + (s, e) => s.PlatformImpl?.SetSystemDecorations((SystemDecorations)e.NewValue)); ShowInTaskbarProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); @@ -140,7 +170,6 @@ namespace Avalonia.Controls MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); MaxWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); MaxHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue))); - } /// @@ -192,12 +221,23 @@ namespace Avalonia.Controls /// Enables or disables system window decorations (title bar, buttons, etc) /// /// + [Obsolete("Use SystemDecorations instead")] public bool HasSystemDecorations { get { return GetValue(HasSystemDecorationsProperty); } set { SetValue(HasSystemDecorationsProperty, value); } } + /// + /// Sets the system decorations (title bar, border, etc) + /// + /// + public SystemDecorations SystemDecorations + { + get { return GetValue(SystemDecorationsProperty); } + set { SetValue(SystemDecorationsProperty, value); } + } + /// /// Enables or disables the taskbar icon /// diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 86e34ca6d4..7480b3519c 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -96,7 +96,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 4bba5ef41b..7bf1d236bd 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -110,7 +110,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index c757576017..a540b026fa 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -68,9 +68,9 @@ namespace Avalonia.Native _native.CanResize = value; } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { - _native.HasDecorations = enabled; + _native.HasDecorations = (int)enabled; } public void SetTitleBarColor (Avalonia.Media.Color color) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 919abae243..b091ee212f 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -173,6 +173,7 @@ namespace Avalonia.X11 Surfaces = surfaces.ToArray(); UpdateMotifHints(); + UpdateSizeHints(null); _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing, XNames.XNClientWindow, _handle, IntPtr.Zero); XFlush(_x11.Display); @@ -219,12 +220,16 @@ namespace Avalonia.X11 var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border | MotifDecorations.Maximize | MotifDecorations.Minimize | MotifDecorations.ResizeH; - if (_popup || !_systemDecorations) + if (_popup || _systemDecorations == SystemDecorations.None) { decorations = 0; } + else if (_systemDecorations == SystemDecorations.BorderOnly) + { + decorations = MotifDecorations.Border; + } - if (!_canResize) + if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly) { functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize); decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH); @@ -247,7 +252,7 @@ namespace Avalonia.X11 var min = _minMaxSize.minSize; var max = _minMaxSize.maxSize; - if (!_canResize) + if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly) max = min = _realSize; if (preResize.HasValue) @@ -621,7 +626,7 @@ namespace Avalonia.X11 return rv; } - private bool _systemDecorations = true; + private SystemDecorations _systemDecorations = SystemDecorations.Full; private bool _canResize = true; private const int MaxWindowDimension = 100000; @@ -777,10 +782,11 @@ namespace Avalonia.X11 (int)(point.X * Scaling + Position.X), (int)(point.Y * Scaling + Position.Y)); - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { _systemDecorations = enabled; UpdateMotifHints(); + UpdateSizeHints(null); } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 904e122382..50b568cab2 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1298,7 +1298,17 @@ namespace Avalonia.Win32.Interop [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, PreserveSig = false)] internal static extern void DoDragDrop(IOleDataObject dataObject, IDropSource dropSource, int allowedEffects, out int finalEffect); + [DllImport("dwmapi.dll")] + public static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); + [StructLayout(LayoutKind.Sequential)] + internal struct MARGINS + { + public int cxLeftWidth; + public int cxRightWidth; + public int cyTopHeight; + public int cyBottomHeight; + } public enum MONITOR { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index c16b76b539..d13c07279c 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -34,7 +34,7 @@ namespace Avalonia.Win32 private IInputRoot _owner; private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock(); private bool _trackingMouse; - private bool _decorated = true; + private SystemDecorations _decorated = SystemDecorations.Full; private bool _resizable = true; private bool _topmost = false; private bool _taskbarIcon = true; @@ -97,7 +97,7 @@ namespace Avalonia.Win32 { get { - if (_decorated) + if (_decorated == SystemDecorations.Full) { var style = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); var exStyle = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE); @@ -281,7 +281,7 @@ namespace Avalonia.Win32 UnmanagedMethods.ShowWindow(_hwnd, UnmanagedMethods.ShowWindowCommand.Hide); } - public void SetSystemDecorations(bool value) + public void SetSystemDecorations(SystemDecorations value) { if (value == _decorated) { @@ -464,7 +464,7 @@ namespace Avalonia.Win32 return IntPtr.Zero; case WindowsMessage.WM_NCCALCSIZE: - if (ToInt32(wParam) == 1 && !_decorated) + if (ToInt32(wParam) == 1 && _decorated != SystemDecorations.Full) { return IntPtr.Zero; } @@ -682,14 +682,14 @@ namespace Avalonia.Win32 break; case WindowsMessage.WM_NCPAINT: - if (!_decorated) + if (_decorated != SystemDecorations.Full) { return IntPtr.Zero; } break; case WindowsMessage.WM_NCACTIVATE: - if (!_decorated) + if (_decorated != SystemDecorations.Full) { return new IntPtr(1); } @@ -1001,7 +1001,7 @@ namespace Avalonia.Win32 style |= WindowStyles.WS_OVERLAPPEDWINDOW; - if (!_decorated) + if (_decorated != SystemDecorations.Full) { style ^= (WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU); } @@ -1011,6 +1011,10 @@ namespace Avalonia.Win32 style ^= (WindowStyles.WS_SIZEFRAME); } + MARGINS margins = new MARGINS(); + margins.cyBottomHeight = _decorated == SystemDecorations.BorderOnly ? 1 : 0; + UnmanagedMethods.DwmExtendFrameIntoClientArea(_hwnd, ref margins); + GetClientRect(_hwnd, out var oldClientRect); var oldClientRectOrigin = new UnmanagedMethods.POINT(); ClientToScreen(_hwnd, ref oldClientRectOrigin); @@ -1024,7 +1028,7 @@ namespace Avalonia.Win32 if (oldDecorated != _decorated) { var newRect = oldClientRect; - if (_decorated) + if (_decorated == SystemDecorations.Full) AdjustWindowRectEx(ref newRect, (uint)style, false, GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE)); SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height, diff --git a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs index 65a6c15971..838bf49846 100644 --- a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs +++ b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs @@ -20,7 +20,7 @@ namespace Avalonia.iOS return Disposable.Empty; } - public void SetSystemDecorations(bool enabled) + public void SetSystemDecorations(SystemDecorations enabled) { } From d44ad423a0b2fec410a3917dcfb27d2187e9c8b4 Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Mon, 10 Feb 2020 09:08:33 +0000 Subject: [PATCH 002/298] Use enum in macOS native --- native/Avalonia.Native/src/OSX/window.h | 6 ++ native/Avalonia.Native/src/OSX/window.mm | 100 +++++++++++++---------- 2 files changed, 64 insertions(+), 42 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 3e626675d2..23e3c22db7 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -36,4 +36,10 @@ struct IWindowStateChanged virtual void WindowStateChanged () = 0; }; +typedef NS_ENUM(NSInteger, SystemDecorations) { + SystemDecorationsNone = 0, + SystemDecorationsBorderOnly = 1, + SystemDecorationsFull = 2, +}; + #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 317d03162b..4c70f661b7 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -410,7 +410,7 @@ class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, pub { private: bool _canResize = true; - int _hasDecorations = 2; + SystemDecorations _hasDecorations = SystemDecorationsFull; CGRect _lastUndecoratedFrame; AvnWindowState _lastWindowState; @@ -475,23 +475,26 @@ private: bool IsZoomed () { - return _hasDecorations > 0 ? [Window isZoomed] : UndecoratedIsMaximized(); + return _hasDecorations != SystemDecorationsNone ? [Window isZoomed] : UndecoratedIsMaximized(); } void DoZoom() { - if (_hasDecorations > 0) + switch (_hasDecorations) { - [Window performZoom:Window]; - } - else - { - if (!UndecoratedIsMaximized()) - { - _lastUndecoratedFrame = [Window frame]; - } - - [Window zoom:Window]; + case SystemDecorationsNone: + if (!UndecoratedIsMaximized()) + { + _lastUndecoratedFrame = [Window frame]; + } + + [Window zoom:Window]; + break; + + case SystemDecorationsBorderOnly: + case SystemDecorationsFull: + [Window performZoom:Window]; + break; } } @@ -509,32 +512,31 @@ private: { @autoreleasepool { - _hasDecorations = value; + _hasDecorations = (SystemDecorations)value; UpdateStyle(); - - // full - if (_hasDecorations == 2) - { - [Window setHasShadow:YES]; - [Window setTitleVisibility:NSWindowTitleVisible]; - [Window setTitlebarAppearsTransparent:NO]; - [Window setTitle:_lastTitle]; - } - // border only - else if (_hasDecorations == 1) - { - [Window setHasShadow:YES]; - [Window setTitleVisibility:NSWindowTitleHidden]; - [Window setTitlebarAppearsTransparent:YES]; - } - // none - else + + switch (_hasDecorations) { - [Window setHasShadow:NO]; - [Window setTitleVisibility:NSWindowTitleHidden]; - [Window setTitlebarAppearsTransparent:YES]; + case SystemDecorationsNone: + [Window setHasShadow:NO]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + break; + + case SystemDecorationsBorderOnly: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleHidden]; + [Window setTitlebarAppearsTransparent:YES]; + break; + + case SystemDecorationsFull: + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleVisible]; + [Window setTitlebarAppearsTransparent:NO]; + [Window setTitle:_lastTitle]; + break; } - + return S_OK; } } @@ -666,12 +668,26 @@ protected: virtual NSWindowStyleMask GetStyle() override { unsigned long s = NSWindowStyleMaskBorderless; - if(_hasDecorations == 1) - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; - if(_hasDecorations == 2) - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; - if(_hasDecorations == 2 && _canResize) - s = s | NSWindowStyleMaskResizable; + + switch (_hasDecorations) + { + case SystemDecorationsNone: + break; + + case SystemDecorationsBorderOnly: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + break; + + case SystemDecorationsFull: + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; + if(_canResize) + { + s = s | NSWindowStyleMaskResizable; + } + + break; + } + return s; } }; From c0e337d61f1df5a8b53135b1179c449b3f6544dc Mon Sep 17 00:00:00 2001 From: Nathan Garside Date: Mon, 10 Feb 2020 09:44:20 +0000 Subject: [PATCH 003/298] Use enum in mac interop --- native/Avalonia.Native/inc/avalonia-native.h | 8 +++++++- native/Avalonia.Native/src/OSX/window.h | 6 ------ native/Avalonia.Native/src/OSX/window.mm | 4 ++-- src/Avalonia.Native/WindowImpl.cs | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index ce4a592d67..ee57f54e59 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -25,6 +25,12 @@ struct IAvnGlSurfaceRenderingSession; struct IAvnAppMenu; struct IAvnAppMenuItem; +enum SystemDecorations { + SystemDecorationsNone = 0, + SystemDecorationsBorderOnly = 1, + SystemDecorationsFull = 2, +}; + struct AvnSize { double Width, Height; @@ -236,7 +242,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase { virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; virtual HRESULT SetCanResize(bool value) = 0; - virtual HRESULT SetHasDecorations(int value) = 0; + virtual HRESULT SetHasDecorations(SystemDecorations value) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0; virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0; diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 23e3c22db7..3e626675d2 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -36,10 +36,4 @@ struct IWindowStateChanged virtual void WindowStateChanged () = 0; }; -typedef NS_ENUM(NSInteger, SystemDecorations) { - SystemDecorationsNone = 0, - SystemDecorationsBorderOnly = 1, - SystemDecorationsFull = 2, -}; - #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 4c70f661b7..2c03407732 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -508,11 +508,11 @@ private: } } - virtual HRESULT SetHasDecorations(int value) override + virtual HRESULT SetHasDecorations(SystemDecorations value) override { @autoreleasepool { - _hasDecorations = (SystemDecorations)value; + _hasDecorations = value; UpdateStyle(); switch (_hasDecorations) diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index a540b026fa..73ec81ce57 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -68,9 +68,9 @@ namespace Avalonia.Native _native.CanResize = value; } - public void SetSystemDecorations(SystemDecorations enabled) + public void SetSystemDecorations(Controls.SystemDecorations enabled) { - _native.HasDecorations = (int)enabled; + _native.HasDecorations = (Interop.SystemDecorations)enabled; } public void SetTitleBarColor (Avalonia.Media.Color color) From f564fd4ed9bf302f13c88d1a15ae7e2ff3f3b9e4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 13 Feb 2020 12:40:57 +0100 Subject: [PATCH 004/298] Update readme.md --- readme.md | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/readme.md b/readme.md index 42b1e52205..40471b3b28 100644 --- a/readme.md +++ b/readme.md @@ -8,25 +8,21 @@ ## About -**Avalonia** is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS and with experimental support for Android and iOS. +**Avalonia** is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS. -**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and [breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) as we continue along into this project's development. To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). +**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development. -| Control catalog | Desktop platforms | Mobile platforms | -|---|---|---| -| | | | +To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239). -[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is curated list of awesome Avalonia UI tools, libraries, projects and resources. +You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. -## Getting Started - -Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (screenshot). Now you can write code and markup that will work on multiple platforms! +[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia! -For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core). +## Getting Started -If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen. +The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starer guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project). -Avalonia is delivered via NuGet package manager. You can find the packages here: [stable(ish)](https://www.nuget.org/packages/Avalonia/) +Avalonia is delivered via NuGet package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/ Use these commands in the Package Manager console to install Avalonia manually: ``` @@ -34,18 +30,17 @@ Install-Package Avalonia Install-Package Avalonia.Desktop ``` -## Bleeding Edge Builds +## JetBrains Rider -or use nightly build feeds as described here: -https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed +If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen. -## Documentation +## Bleeding Edge Builds -You can take a look at the [getting started page](http://avaloniaui.net/docs/quickstart/) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia). +We also have a [nightly build](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed) which tracks the current state of master. Although these packages are less stable than the release on NuGet.org, you'll get all the latest features and bugfixes right away and many of our users actually prefer this feed! -There's also a high-level [architecture document](http://avaloniaui.net/architecture/project-structure) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/. +## Documentation -Contributions for our docs are always welcome! +Documentation can be found on our website at http://avaloniaui.net/docs/. We also have a [tutorial](http://avaloniaui.net/docs/tutorial/) over there for newcomers. ## Building and Using @@ -60,14 +55,12 @@ Please read the [contribution guidelines](http://avaloniaui.net/contributing/con This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing/contributing)]. - ### Backers Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Avalonia#backer)] - ### Sponsors Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/Avalonia#sponsor)] From c34bfc56f8d3baa8b7f3e6d116e27edaa9a46a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D0=B4=D0=B8=D0=BC=20=D0=9C=D0=B5=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2?= Date: Tue, 18 Feb 2020 16:25:28 +0300 Subject: [PATCH 005/298] Add class CroppedBitmap --- samples/ControlCatalog/Pages/ImagePage.xaml | 15 +++++- .../ControlCatalog/Pages/ImagePage.xaml.cs | 41 ++++++++++++++++ .../Media/Imaging/CroppedBitmap.cs | 47 +++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml b/samples/ControlCatalog/Pages/ImagePage.xaml index 9b8f8af765..c20f76cedd 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml +++ b/samples/ControlCatalog/Pages/ImagePage.xaml @@ -7,7 +7,7 @@ Displays an image - + Bitmap @@ -22,6 +22,19 @@ + Crop + + None + Center + TopLeft + TopRight + BottomLeft + BottomRight + + + + + Drawing None diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml.cs b/samples/ControlCatalog/Pages/ImagePage.xaml.cs index bbe89d1dfd..d637c88102 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml.cs +++ b/samples/ControlCatalog/Pages/ImagePage.xaml.cs @@ -1,6 +1,10 @@ +using System; +using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; namespace ControlCatalog.Pages { @@ -8,12 +12,17 @@ namespace ControlCatalog.Pages { private readonly Image _bitmapImage; private readonly Image _drawingImage; + private readonly Image _croppedImage; + private readonly IBitmap _croppedBitmapSource; public ImagePage() { InitializeComponent(); _bitmapImage = this.FindControl("bitmapImage"); _drawingImage = this.FindControl("drawingImage"); + _croppedImage = this.FindControl("croppedImage"); + _croppedBitmapSource = LoadBitmap("avares://ControlCatalog/Assets/delicate-arch-896885_640.jpg"); + _croppedImage.Source = new CroppedBitmap(_croppedBitmapSource, default); } private void InitializeComponent() @@ -38,5 +47,37 @@ namespace ControlCatalog.Pages _drawingImage.Stretch = (Stretch)comboxBox.SelectedIndex; } } + + public void BitmapCropChanged(object sender, SelectionChangedEventArgs e) + { + if (_croppedImage != null) + { + var comboxBox = (ComboBox)sender; + _croppedImage.Source = new CroppedBitmap( _croppedBitmapSource, GetCropRect(comboxBox.SelectedIndex)); + } + } + + private PixelRect GetCropRect(int index) + { + var bitmapWidth = _croppedBitmapSource.PixelSize.Width; + var bitmapHeight = _croppedBitmapSource.PixelSize.Height; + var cropSize = new PixelSize(bitmapWidth / 2, bitmapHeight / 2); + return index switch + { + 1 => new PixelRect(new PixelPoint((bitmapWidth - cropSize.Width) / 2, (bitmapHeight - cropSize.Width) / 2), cropSize), + 2 => new PixelRect(new PixelPoint(0, 0), cropSize), + 3 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, 0), cropSize), + 4 => new PixelRect(new PixelPoint(0, bitmapHeight - cropSize.Height), cropSize), + 5 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, bitmapHeight - cropSize.Height), cropSize), + _ => PixelRect.Empty + }; + + } + + private IBitmap LoadBitmap(string uri) + { + var assets = AvaloniaLocator.Current.GetService(); + return new Bitmap(assets.Open(new Uri(uri))); + } } } diff --git a/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs new file mode 100644 index 0000000000..6bdee24c03 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Media.Imaging +{ + public class CroppedBitmap : IImage, IDisposable + { + public CroppedBitmap() + { + Source = null; + SourceRect = default; + } + public CroppedBitmap(IBitmap source, PixelRect sourceRect) + { + Source = source; + SourceRect = sourceRect; + } + public virtual void Dispose() + { + Source?.Dispose(); + } + + public Size Size { + get + { + if (Source == null) + return Size.Empty; + if (SourceRect.IsEmpty) + return Source.Size; + return SourceRect.Size.ToSizeWithDpi(Source.Dpi); + } + } + + public void Draw(DrawingContext context, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) + { + if (Source == null) + return; + var topLeft = SourceRect.TopLeft.ToPointWithDpi(Source.Dpi); + Source.Draw(context, sourceRect.Translate(new Vector(topLeft.X, topLeft.Y)), destRect, bitmapInterpolationMode); + } + + public IBitmap Source { get; } + public PixelRect SourceRect { get; } + } +} From e870f6c6e492f7df704d491c0a7b310d2bdffefd Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Thu, 20 Feb 2020 14:00:39 +0100 Subject: [PATCH 006/298] Update NUKE to 0.24 --- nukebuild/Build.cs | 54 ++++++++++++++++++++++------------------- nukebuild/Shims.cs | 8 +++--- nukebuild/_build.csproj | 8 +++--- 3 files changed, 37 insertions(+), 33 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 7b3b8465ce..b14b78065b 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -26,7 +26,7 @@ using static Nuke.Common.Tools.VSWhere.VSWhereTasks; running and debugging a particular target (optionally without deps) would be way easier ReSharper/Rider - https://plugins.jetbrains.com/plugin/10803-nuke-support VSCode - https://marketplace.visualstudio.com/items?itemName=nuke.support - + */ partial class Build : NukeBuild @@ -54,7 +54,7 @@ partial class Build : NukeBuild protected override void OnBuildInitialized() { Parameters = new BuildParameters(this); - Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.", + Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.", Parameters.Version, Parameters.Configuration, typeof(NukeBuild).Assembly.GetName().Version.ToString()); @@ -93,8 +93,10 @@ partial class Build : NukeBuild string projectFile, Configure configurator = null) { - return MSBuild(projectFile, c => + return MSBuild(c => { + c = c.SetProjectFile(projectFile); + // This is required for VS2019 image on Azure Pipelines if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure) { @@ -114,8 +116,8 @@ partial class Build : NukeBuild } Target Clean => _ => _.Executes(() => { - DeleteDirectories(Parameters.BuildDirs); - EnsureCleanDirectories(Parameters.BuildDirs); + Parameters.BuildDirs.ForEach(DeleteDirectory); + Parameters.BuildDirs.ForEach(DeleteDirectory); EnsureCleanDirectory(Parameters.ArtifactsDir); EnsureCleanDirectory(Parameters.NugetIntermediateRoot); EnsureCleanDirectory(Parameters.NugetRoot); @@ -134,12 +136,13 @@ partial class Build : NukeBuild ); else - DotNetBuild(Parameters.MSBuildSolution, c => c + DotNetBuild(c => c + .SetProjectFile(Parameters.MSBuildSolution) .AddProperty("PackageVersion", Parameters.Version) .SetConfiguration(Parameters.Configuration) ); }); - + void RunCoreTest(string project) { if(!project.EndsWith(".csproj")) @@ -153,13 +156,13 @@ partial class Build : NukeBuild var targets = xdoc.Root.Descendants("TargetFrameworks").FirstOrDefault(); if (targets != null) frameworks = targets.Value.Split(';').Where(f => !string.IsNullOrWhiteSpace(f)).ToList(); - else + else frameworks = new List {xdoc.Root.Descendants("TargetFramework").First().Value}; - + foreach(var fw in frameworks) { if (fw.StartsWith("net4") - && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") { Information($"Skipping {fw} tests on Linux - https://github.com/mono/mono/issues/13969"); @@ -184,7 +187,7 @@ partial class Build : NukeBuild } Target RunCoreLibsTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests) + .OnlyWhenStatic(() => !Parameters.SkipTests) .DependsOn(Compile) .Executes(() => { @@ -204,7 +207,7 @@ partial class Build : NukeBuild }); Target RunRenderTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests) + .OnlyWhenStatic(() => !Parameters.SkipTests) .DependsOn(Compile) .Executes(() => { @@ -212,9 +215,9 @@ partial class Build : NukeBuild if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj"); }); - + Target RunDesignerTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) + .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .DependsOn(Compile) .Executes(() => { @@ -224,7 +227,7 @@ partial class Build : NukeBuild [PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit; Target RunLeakTests => _ => _ - .OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) + .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .DependsOn(Compile) .Executes(() => { @@ -235,7 +238,7 @@ partial class Build : NukeBuild }); Target ZipFiles => _ => _ - .After(CreateNugetPackages, Compile, RunCoreLibsTests, Package) + .After(CreateNugetPackages, Compile, RunCoreLibsTests, Package) .Executes(() => { var data = Parameters; @@ -259,9 +262,10 @@ partial class Build : NukeBuild MsBuildCommon(Parameters.MSBuildSolution, c => c .AddTargets("Pack")); else - DotNetPack(Parameters.MSBuildSolution, c => - c.SetConfiguration(Parameters.Configuration) - .AddProperty("PackageVersion", Parameters.Version)); + DotNetPack(c => c + .SetProject(Parameters.MSBuildSolution) + .SetConfiguration(Parameters.Configuration) + .AddProperty("PackageVersion", Parameters.Version)); }); Target CreateNugetPackages => _ => _ @@ -274,29 +278,29 @@ partial class Build : NukeBuild new NumergeNukeLogger())) throw new Exception("Package merge failed"); }); - + Target RunTests => _ => _ .DependsOn(RunCoreLibsTests) .DependsOn(RunRenderTests) .DependsOn(RunDesignerTests) .DependsOn(RunLeakTests); - + Target Package => _ => _ .DependsOn(RunTests) .DependsOn(CreateNugetPackages); - + Target CiAzureLinux => _ => _ .DependsOn(RunTests); - + Target CiAzureOSX => _ => _ .DependsOn(Package) .DependsOn(ZipFiles); - + Target CiAzureWindows => _ => _ .DependsOn(Package) .DependsOn(ZipFiles); - + public static int Main() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Execute(x => x.Package) diff --git a/nukebuild/Shims.cs b/nukebuild/Shims.cs index 461d617643..1ac14bf622 100644 --- a/nukebuild/Shims.cs +++ b/nukebuild/Shims.cs @@ -19,9 +19,9 @@ public partial class Build Logger.Info(info, args); } - private void Zip(PathConstruction.AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable()); + private void Zip(AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable()); - private void Zip(PathConstruction.AbsolutePath target, IEnumerable paths) + private void Zip(AbsolutePath target, IEnumerable paths) { var targetPath = target.ToString(); bool finished = false, atLeastOneFileAdded = false; @@ -38,7 +38,7 @@ public partial class Build fileStream.CopyTo(entryStream); atLeastOneFileAdded = true; } - + foreach (var path in paths) { if (Directory.Exists(path)) @@ -64,7 +64,7 @@ public partial class Build finished = true; } - finally + finally { try { diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 2a736e4653..f26bf7137e 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + netcoreapp3.0 false False @@ -10,7 +10,7 @@ - + @@ -20,11 +20,11 @@ - + - + From 367548c1bece9d3bfc4b9d54bddaeb0fd55e6e12 Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Thu, 20 Feb 2020 14:21:55 +0100 Subject: [PATCH 007/298] Minor cleanups --- nukebuild/Build.cs | 118 +++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 64 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index b14b78065b..1a924733b2 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -13,6 +13,7 @@ using Nuke.Common.Tooling; using Nuke.Common.Tools.DotNet; using Nuke.Common.Tools.MSBuild; using Nuke.Common.Utilities; +using Nuke.Common.Utilities.Collections; using static Nuke.Common.EnvironmentInfo; using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.PathConstruction; @@ -31,6 +32,8 @@ using static Nuke.Common.Tools.VSWhere.VSWhereTasks; partial class Build : NukeBuild { + [Solution("Avalonia.sln")] readonly Solution Solution; + static Lazy MsBuildExe = new Lazy(() => { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -93,27 +96,20 @@ partial class Build : NukeBuild string projectFile, Configure configurator = null) { - return MSBuild(c => - { - c = c.SetProjectFile(projectFile); - + return MSBuild(c => c + .SetProjectFile(projectFile) // This is required for VS2019 image on Azure Pipelines - if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure) - { - var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64"); - if (javaSdk != null) - c = c.AddProperty("JavaSdkDirectory", javaSdk); - } - - c = c.AddProperty("PackageVersion", Parameters.Version) - .AddProperty("iOSRoslynPathHackRequired", "true") - .SetToolPath(MsBuildExe.Value) - .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal); - c = configurator?.Invoke(c) ?? c; - return c; - }); + .When(Parameters.IsRunningOnWindows && + Parameters.IsRunningOnAzure, c => c + .AddProperty("JavaSdkDirectory", GetVariable("JAVA_HOME_8_X64"))) + .AddProperty("PackageVersion", Parameters.Version) + .AddProperty("iOSRoslynPathHackRequired", true) + .SetToolPath(MsBuildExe.Value) + .SetConfiguration(Parameters.Configuration) + .SetVerbosity(MSBuildVerbosity.Minimal) + .Apply(configurator)); } + Target Clean => _ => _.Executes(() => { Parameters.BuildDirs.ForEach(DeleteDirectory); @@ -143,23 +139,12 @@ partial class Build : NukeBuild ); }); - void RunCoreTest(string project) + void RunCoreTest(string projectName) { - if(!project.EndsWith(".csproj")) - project = System.IO.Path.Combine(project, System.IO.Path.GetFileName(project)+".csproj"); - Information("Running tests from " + project); - XDocument xdoc; - using (var s = File.OpenRead(project)) - xdoc = XDocument.Load(s); - - List frameworks = null; - var targets = xdoc.Root.Descendants("TargetFrameworks").FirstOrDefault(); - if (targets != null) - frameworks = targets.Value.Split(';').Where(f => !string.IsNullOrWhiteSpace(f)).ToList(); - else - frameworks = new List {xdoc.Root.Descendants("TargetFramework").First().Value}; - - foreach(var fw in frameworks) + Information($"Running tests from {projectName}"); + var project = Solution.GetProject(projectName).NotNull("project != null"); + + foreach (var fw in project.GetTargetFrameworks()) { if (fw.StartsWith("net4") && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) @@ -170,19 +155,16 @@ partial class Build : NukeBuild } Information("Running for " + fw); - DotNetTest(c => - { - c = c - .SetProjectFile(project) - .SetConfiguration(Parameters.Configuration) - .SetFramework(fw) - .EnableNoBuild() - .EnableNoRestore(); - // NOTE: I can see that we could maybe add another extension method "Switch" or "If" to make this more convenient - if (Parameters.PublishTestResults) - c = c.SetLogger("trx").SetResultsDirectory(Parameters.TestResultsRoot); - return c; - }); + + DotNetTest(c => c + .SetProjectFile(project) + .SetConfiguration(Parameters.Configuration) + .SetFramework(fw) + .EnableNoBuild() + .EnableNoRestore() + .When(Parameters.PublishTestResults, c => c + .SetLogger("trx") + .SetResultsDirectory(Parameters.TestResultsRoot))); } } @@ -191,19 +173,19 @@ partial class Build : NukeBuild .DependsOn(Compile) .Executes(() => { - RunCoreTest("./tests/Avalonia.Animation.UnitTests"); - RunCoreTest("./tests/Avalonia.Base.UnitTests"); - RunCoreTest("./tests/Avalonia.Controls.UnitTests"); - RunCoreTest("./tests/Avalonia.Controls.DataGrid.UnitTests"); - RunCoreTest("./tests/Avalonia.Input.UnitTests"); - RunCoreTest("./tests/Avalonia.Interactivity.UnitTests"); - RunCoreTest("./tests/Avalonia.Layout.UnitTests"); - RunCoreTest("./tests/Avalonia.Markup.UnitTests"); - RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests"); - RunCoreTest("./tests/Avalonia.Styling.UnitTests"); - RunCoreTest("./tests/Avalonia.Visuals.UnitTests"); - RunCoreTest("./tests/Avalonia.Skia.UnitTests"); - RunCoreTest("./tests/Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.Animation.UnitTests"); + RunCoreTest("Avalonia.Base.UnitTests"); + RunCoreTest("Avalonia.Controls.UnitTests"); + RunCoreTest("Avalonia.Controls.DataGrid.UnitTests"); + RunCoreTest("Avalonia.Input.UnitTests"); + RunCoreTest("Avalonia.Interactivity.UnitTests"); + RunCoreTest("Avalonia.Layout.UnitTests"); + RunCoreTest("Avalonia.Markup.UnitTests"); + RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); + RunCoreTest("Avalonia.Styling.UnitTests"); + RunCoreTest("Avalonia.Visuals.UnitTests"); + RunCoreTest("Avalonia.Skia.UnitTests"); + RunCoreTest("Avalonia.ReactiveUI.UnitTests"); }); Target RunRenderTests => _ => _ @@ -211,9 +193,9 @@ partial class Build : NukeBuild .DependsOn(Compile) .Executes(() => { - RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj"); + RunCoreTest("Avalonia.Skia.RenderTests"); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj"); + RunCoreTest("Avalonia.Direct2D1.RenderTests"); }); Target RunDesignerTests => _ => _ @@ -221,7 +203,7 @@ partial class Build : NukeBuild .DependsOn(Compile) .Executes(() => { - RunCoreTest("./tests/Avalonia.DesignerSupport.Tests"); + RunCoreTest("Avalonia.DesignerSupport.Tests"); }); [PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit; @@ -307,3 +289,11 @@ partial class Build : NukeBuild : Execute(x => x.RunTests); } + +public static class ToolSettingsExtensions +{ + public static T Apply(this T settings, Configure configurator) + { + return configurator != null ? configurator(settings) : settings; + } +} From d60603d52968ad5c89ab7ef6594a57caf002474d Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Fri, 21 Feb 2020 11:55:54 +0100 Subject: [PATCH 008/298] Fix information output --- nukebuild/Build.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 1a924733b2..c2fa54ba2b 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -150,11 +150,11 @@ partial class Build : NukeBuild && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1") { - Information($"Skipping {fw} tests on Linux - https://github.com/mono/mono/issues/13969"); + Information($"Skipping {projectName} ({fw}) tests on Linux - https://github.com/mono/mono/issues/13969"); continue; } - Information("Running for " + fw); + Information($"Running for {projectName} ({fw}) ..."); DotNetTest(c => c .SetProjectFile(project) From 8f68d2d12407dfe1b41bfaa7fdd728f76f8ccc6c Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Fri, 21 Feb 2020 11:56:34 +0100 Subject: [PATCH 009/298] Fix ensuring directory --- nukebuild/Build.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index c2fa54ba2b..358846a14e 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -113,7 +113,7 @@ partial class Build : NukeBuild Target Clean => _ => _.Executes(() => { Parameters.BuildDirs.ForEach(DeleteDirectory); - Parameters.BuildDirs.ForEach(DeleteDirectory); + Parameters.BuildDirs.ForEach(EnsureCleanDirectory); EnsureCleanDirectory(Parameters.ArtifactsDir); EnsureCleanDirectory(Parameters.NugetIntermediateRoot); EnsureCleanDirectory(Parameters.NugetRoot); From 924064be139c9a2847d066aba19e3d5896219b42 Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Fri, 21 Feb 2020 12:31:39 +0100 Subject: [PATCH 010/298] Use netcoreapp3.1 --- nukebuild/_build.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index f26bf7137e..584c36d033 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.0 + netcoreapp3.1 false False From 2bf2e60ae04604ded6bd0dbce0807ca1b8762711 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 22 Feb 2020 10:00:45 +0100 Subject: [PATCH 011/298] Don't add extra pixel to AccessText measurement. It's not needed; the underscore can be drawn in the descender space. --- src/Avalonia.Controls/Primitives/AccessText.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 5adc8d2448..f6fea89ec9 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -84,17 +84,6 @@ namespace Avalonia.Controls.Primitives return base.CreateFormattedText(constraint, StripAccessKey(text)); } - /// - /// Measures the control. - /// - /// The available size for the control. - /// The desired size. - protected override Size MeasureOverride(Size availableSize) - { - var result = base.MeasureOverride(availableSize); - return result.WithHeight(result.Height + 1); - } - /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { From 8a0ccea2731e2df48d105f9e5fea25e6f1361fb9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 22 Feb 2020 10:02:19 +0100 Subject: [PATCH 012/298] Add MenuItem.InputGestureText. --- src/Avalonia.Controls/MenuItem.cs | 51 +++++++++++++++++++++++ src/Avalonia.Themes.Default/MenuItem.xaml | 26 ++++++++---- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index e0baa5e679..ae36b5d830 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -13,6 +13,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -48,6 +49,12 @@ namespace Avalonia.Controls public static readonly StyledProperty IconProperty = AvaloniaProperty.Register(nameof(Icon)); + /// + /// Defines the property. + /// + public static readonly StyledProperty InputGestureTextProperty = + AvaloniaProperty.Register(nameof(InputGestureText)); + /// /// Defines the property. /// @@ -93,6 +100,7 @@ namespace Avalonia.Controls private ICommand _command; private bool _commandCanExecute = true; private Popup _popup; + private IDisposable _gridHack; /// /// Initializes static members of the class. @@ -194,6 +202,19 @@ namespace Avalonia.Controls set { SetValue(IconProperty, value); } } + /// + /// Gets or sets the input gesture that will be displayed in the menu item. + /// + /// + /// Setting this property does not cause the input gesture to be handled by the menu item, + /// it simply displays the gesture text in the menu. + /// + public object InputGestureText + { + get { return GetValue(InputGestureTextProperty); } + set { SetValue(InputGestureTextProperty, value); } + } + /// /// Gets or sets a value indicating whether the is currently selected. /// @@ -306,6 +327,36 @@ namespace Avalonia.Controls } } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + if (this.GetVisualParent() is IControl parent) + { + // HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached + // property to store SharedSizeGroup state, except property inheritance is done + // down the logical tree. In this case, the control which is setting + // Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing + // the way Grid stores shared size state, the developers of WPF just created a + // binding of the internal state of the visual parent to the menu item. We don't + // have much choice but to do the same for now unless we want to refactor Grid, + // which I honestly am not brave enough to do right now. Here's the same hack in + // the WPF codebase: + // + // https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141 + _gridHack = Bind( + DefinitionBase.PrivateSharedSizeScopeProperty, + parent.GetBindingObservable(DefinitionBase.PrivateSharedSizeScopeProperty)); + } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _gridHack.Dispose(); + _gridHack = null; + } + /// /// Called when the is clicked. /// diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 93989d3782..431adacb47 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -11,7 +11,14 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> - + + + + + + + + + + Grid.Column="4"/> + Margin="4 2" + Grid.IsSharedSizeScope="True"/> @@ -102,10 +113,11 @@ BorderThickness="{TemplateBinding BorderThickness}"> + Items="{TemplateBinding Items}" + ItemsPanel="{TemplateBinding ItemsPanel}" + ItemTemplate="{TemplateBinding ItemTemplate}" + Margin="2" + Grid.IsSharedSizeScope="True"/> From deebe6090ffddfff6e5da007d9c56a68d2ffe5a5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 22 Feb 2020 10:02:33 +0100 Subject: [PATCH 013/298] Show input gesture text in control catalog. --- samples/ControlCatalog/Pages/MenuPage.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index 868f0df6ad..cae5ab54b1 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -16,13 +16,13 @@ Defined in XAML - + - + From 467288ca998a5585955d8cbcf63903c5b3a7c4de Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Sat, 22 Feb 2020 18:42:25 +0100 Subject: [PATCH 014/298] Update NUKE to 0.24 --- nukebuild/BuildParameters.cs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index 65ba5e9756..149716b416 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -4,24 +4,21 @@ using System.Linq; using System.Runtime.InteropServices; using System.Xml.Linq; using Nuke.Common; -using Nuke.Common.BuildServers; -using Nuke.Common.Execution; +using Nuke.Common.CI.AzurePipelines; using Nuke.Common.IO; -using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.PathConstruction; -using static Nuke.Common.Tools.MSBuild.MSBuildTasks; public partial class Build { [Parameter("configuration")] public string Configuration { get; set; } - + [Parameter("skip-tests")] public bool SkipTests { get; set; } - + [Parameter("force-nuget-version")] public string ForceNugetVersion { get; set; } - + public class BuildParameters { public string Configuration { get; } @@ -79,15 +76,15 @@ public partial class Build IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX; IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - IsRunningOnAzure = Host == HostType.TeamServices || + IsRunningOnAzure = Host == HostType.AzurePipelines || Environment.GetEnvironmentVariable("LOGNAME") == "vsts"; if (IsRunningOnAzure) { - RepositoryName = TeamServices.Instance.RepositoryUri; - RepositoryBranch = TeamServices.Instance.SourceBranch; - IsPullRequest = TeamServices.Instance.PullRequestId.HasValue; - IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, TeamServices.Instance.RepositoryUri); + RepositoryName = AzurePipelines.Instance.RepositoryUri; + RepositoryBranch = AzurePipelines.Instance.SourceBranch; + IsPullRequest = AzurePipelines.Instance.PullRequestId.HasValue; + IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, AzurePipelines.Instance.RepositoryUri); } IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, From b966bd390c7d310590ef98baa0c9aa69440cacce Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 23 Feb 2020 15:34:04 +0100 Subject: [PATCH 015/298] Enable NRT in Avalonia.Interactivity. --- .../Avalonia.Interactivity.csproj | 6 +- .../EventSubscription.cs | 20 ++++-- src/Avalonia.Interactivity/IInteractive.cs | 2 +- src/Avalonia.Interactivity/Interactive.cs | 68 ++++++++----------- .../InteractiveExtensions.cs | 5 +- src/Avalonia.Interactivity/RoutedEvent.cs | 18 ++--- src/Avalonia.Interactivity/RoutedEventArgs.cs | 8 +-- .../RoutedEventRegistry.cs | 6 +- 8 files changed, 69 insertions(+), 64 deletions(-) diff --git a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj index 66f1e8cc26..730ca2bd6e 100644 --- a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj +++ b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj @@ -1,6 +1,8 @@  netstandard2.0 + Enable + CS8600;CS8602;CS8603 @@ -9,6 +11,4 @@ - - - + \ No newline at end of file diff --git a/src/Avalonia.Interactivity/EventSubscription.cs b/src/Avalonia.Interactivity/EventSubscription.cs index e8fb1bfaf1..d363e3f6fa 100644 --- a/src/Avalonia.Interactivity/EventSubscription.cs +++ b/src/Avalonia.Interactivity/EventSubscription.cs @@ -9,12 +9,24 @@ namespace Avalonia.Interactivity internal class EventSubscription { - public HandlerInvokeSignature InvokeAdapter { get; set; } + public EventSubscription( + Delegate handler, + RoutingStrategies routes, + bool handledEventsToo, + HandlerInvokeSignature? invokeAdapter = null) + { + Handler = handler; + Routes = routes; + HandledEventsToo = handledEventsToo; + InvokeAdapter = invokeAdapter; + } - public Delegate Handler { get; set; } + public HandlerInvokeSignature? InvokeAdapter { get; } - public RoutingStrategies Routes { get; set; } + public Delegate Handler { get; } - public bool AlsoIfHandled { get; set; } + public RoutingStrategies Routes { get; } + + public bool HandledEventsToo { get; } } } diff --git a/src/Avalonia.Interactivity/IInteractive.cs b/src/Avalonia.Interactivity/IInteractive.cs index 47046b58e2..6524794733 100644 --- a/src/Avalonia.Interactivity/IInteractive.cs +++ b/src/Avalonia.Interactivity/IInteractive.cs @@ -13,7 +13,7 @@ namespace Avalonia.Interactivity /// /// Gets the interactive parent of the object for bubbling and tunneling events. /// - IInteractive InteractiveParent { get; } + IInteractive? InteractiveParent { get; } /// /// Adds a handler for the specified routed event. diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 27ece25183..0c4649a1ca 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -15,16 +15,16 @@ namespace Avalonia.Interactivity /// public class Interactive : Layoutable, IInteractive { - private Dictionary> _eventHandlers; + private Dictionary>? _eventHandlers; private static readonly Dictionary s_invokeHandlerCache = new Dictionary(); /// /// Gets the interactive parent of the object for bubbling and tunneling events. /// - IInteractive IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; + IInteractive? IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; - private Dictionary> EventHandlers => _eventHandlers ?? (_eventHandlers = new Dictionary>()); + private Dictionary> EventHandlers => _eventHandlers ??= new Dictionary>(); /// /// Adds a handler for the specified routed event. @@ -40,16 +40,10 @@ namespace Avalonia.Interactivity RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false) { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); - - var subscription = new EventSubscription - { - Handler = handler, - Routes = routes, - AlsoIfHandled = handledEventsToo, - }; + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); + var subscription = new EventSubscription(handler, routes, handledEventsToo); return AddEventSubscription(routedEvent, subscription); } @@ -68,12 +62,12 @@ namespace Avalonia.Interactivity RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false) where TEventArgs : RoutedEventArgs { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); // EventHandler delegate is not covariant, this forces us to create small wrapper // that will cast our type erased instance and invoke it. - Type eventArgsType = routedEvent.EventArgsType; + var eventArgsType = routedEvent.EventArgsType; if (!s_invokeHandlerCache.TryGetValue(eventArgsType, out var invokeAdapter)) { @@ -90,14 +84,7 @@ namespace Avalonia.Interactivity s_invokeHandlerCache.Add(eventArgsType, invokeAdapter); } - var subscription = new EventSubscription - { - InvokeAdapter = invokeAdapter, - Handler = handler, - Routes = routes, - AlsoIfHandled = handledEventsToo, - }; - + var subscription = new EventSubscription(handler, routes, handledEventsToo, invokeAdapter); return AddEventSubscription(routedEvent, subscription); } @@ -108,12 +95,11 @@ namespace Avalonia.Interactivity /// The handler. public void RemoveHandler(RoutedEvent routedEvent, Delegate handler) { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); - - List subscriptions = null; + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); - if (_eventHandlers?.TryGetValue(routedEvent, out subscriptions) == true) + if (_eventHandlers is object && + _eventHandlers.TryGetValue(routedEvent, out var subscriptions) == true) { subscriptions.RemoveAll(x => x.Handler == handler); } @@ -137,9 +123,14 @@ namespace Avalonia.Interactivity /// The event args. public void RaiseEvent(RoutedEventArgs e) { - Contract.Requires(e != null); + e = e ?? throw new ArgumentNullException(nameof(e)); - e.Source = e.Source ?? this; + if (e.RoutedEvent == null) + { + throw new ArgumentException("Cannot raise an event whose RoutedEvent is null."); + } + + e.Source ??= this; if (e.RoutedEvent.RoutingStrategies == RoutingStrategies.Direct) { @@ -167,7 +158,7 @@ namespace Avalonia.Interactivity /// The event args. private void BubbleEvent(RoutedEventArgs e) { - Contract.Requires(e != null); + e = e ?? throw new ArgumentNullException(nameof(e)); e.Route = RoutingStrategies.Bubble; @@ -182,7 +173,7 @@ namespace Avalonia.Interactivity /// The event args. private void TunnelEvent(RoutedEventArgs e) { - Contract.Requires(e != null); + e = e ?? throw new ArgumentNullException(nameof(e)); e.Route = RoutingStrategies.Tunnel; @@ -197,18 +188,17 @@ namespace Avalonia.Interactivity /// The event args. private void RaiseEventImpl(RoutedEventArgs e) { - Contract.Requires(e != null); - - e.RoutedEvent.InvokeRaised(this, e); + e = e ?? throw new ArgumentNullException(nameof(e)); - List subscriptions = null; + e.RoutedEvent!.InvokeRaised(this, e); - if (_eventHandlers?.TryGetValue(e.RoutedEvent, out subscriptions) == true) + if (_eventHandlers is object && + _eventHandlers.TryGetValue(e.RoutedEvent, out var subscriptions) == true) { foreach (var sub in subscriptions.ToList()) { bool correctRoute = (e.Route & sub.Routes) != 0; - bool notFinished = !e.Handled || sub.AlsoIfHandled; + bool notFinished = !e.Handled || sub.HandledEventsToo; if (correctRoute && notFinished) { @@ -313,7 +303,7 @@ namespace Avalonia.Interactivity { _preTraverse.Execute(target, _args); - IInteractive parent = target.InteractiveParent; + var parent = target.InteractiveParent; if (parent != null) { diff --git a/src/Avalonia.Interactivity/InteractiveExtensions.cs b/src/Avalonia.Interactivity/InteractiveExtensions.cs index 07e4029240..414c408080 100644 --- a/src/Avalonia.Interactivity/InteractiveExtensions.cs +++ b/src/Avalonia.Interactivity/InteractiveExtensions.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Linq; using System.Reactive.Linq; namespace Avalonia.Interactivity @@ -30,6 +28,9 @@ namespace Avalonia.Interactivity bool handledEventsToo = false) where TEventArgs : RoutedEventArgs { + o = o ?? throw new ArgumentNullException(nameof(o)); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + return Observable.Create(x => o.AddHandler( routedEvent, (_, e) => x.OnNext(e), diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Interactivity/RoutedEvent.cs index 55d9e61d87..164a86fab7 100644 --- a/src/Avalonia.Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Interactivity/RoutedEvent.cs @@ -25,10 +25,14 @@ namespace Avalonia.Interactivity Type eventArgsType, Type ownerType) { - Contract.Requires(name != null); - Contract.Requires(eventArgsType != null); - Contract.Requires(ownerType != null); - Contract.Requires(typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType)); + name = name ?? throw new ArgumentNullException(nameof(name)); + eventArgsType = eventArgsType ?? throw new ArgumentNullException(nameof(name)); + ownerType = ownerType ?? throw new ArgumentNullException(nameof(name)); + + if (!typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType)) + { + throw new InvalidCastException("eventArgsType must be derived from RoutedEventArgs."); + } EventArgsType = eventArgsType; Name = name; @@ -52,7 +56,7 @@ namespace Avalonia.Interactivity RoutingStrategies routingStrategy) where TEventArgs : RoutedEventArgs { - Contract.Requires(name != null); + name = name ?? throw new ArgumentNullException(nameof(name)); var routedEvent = new RoutedEvent(name, routingStrategy, typeof(TOwner)); RoutedEventRegistry.Instance.Register(typeof(TOwner), routedEvent); @@ -65,7 +69,7 @@ namespace Avalonia.Interactivity Type ownerType) where TEventArgs : RoutedEventArgs { - Contract.Requires(name != null); + name = name ?? throw new ArgumentNullException(nameof(name)); var routedEvent = new RoutedEvent(name, routingStrategy, ownerType); RoutedEventRegistry.Instance.Register(ownerType, routedEvent); @@ -108,8 +112,6 @@ namespace Avalonia.Interactivity public RoutedEvent(string name, RoutingStrategies routingStrategies, Type ownerType) : base(name, routingStrategies, typeof(TEventArgs), ownerType) { - Contract.Requires(name != null); - Contract.Requires(ownerType != null); } [Obsolete("Use overload taking Action.")] diff --git a/src/Avalonia.Interactivity/RoutedEventArgs.cs b/src/Avalonia.Interactivity/RoutedEventArgs.cs index 05bbf7b6a3..e00393322d 100644 --- a/src/Avalonia.Interactivity/RoutedEventArgs.cs +++ b/src/Avalonia.Interactivity/RoutedEventArgs.cs @@ -11,12 +11,12 @@ namespace Avalonia.Interactivity { } - public RoutedEventArgs(RoutedEvent routedEvent) + public RoutedEventArgs(RoutedEvent? routedEvent) { RoutedEvent = routedEvent; } - public RoutedEventArgs(RoutedEvent routedEvent, IInteractive source) + public RoutedEventArgs(RoutedEvent? routedEvent, IInteractive? source) { RoutedEvent = routedEvent; Source = source; @@ -24,10 +24,10 @@ namespace Avalonia.Interactivity public bool Handled { get; set; } - public RoutedEvent RoutedEvent { get; set; } + public RoutedEvent? RoutedEvent { get; set; } public RoutingStrategies Route { get; set; } - public IInteractive Source { get; set; } + public IInteractive? Source { get; set; } } } diff --git a/src/Avalonia.Interactivity/RoutedEventRegistry.cs b/src/Avalonia.Interactivity/RoutedEventRegistry.cs index 34c970a806..0111b115e6 100644 --- a/src/Avalonia.Interactivity/RoutedEventRegistry.cs +++ b/src/Avalonia.Interactivity/RoutedEventRegistry.cs @@ -32,8 +32,8 @@ namespace Avalonia.Interactivity /// public void Register(Type type, RoutedEvent @event) { - Contract.Requires(type != null); - Contract.Requires(@event != null); + type = type ?? throw new ArgumentNullException(nameof(type)); + @event = @event ?? throw new ArgumentNullException(nameof(@event)); if (!_registeredRoutedEvents.TryGetValue(type, out var list)) { @@ -66,7 +66,7 @@ namespace Avalonia.Interactivity /// All routed events registered with the provided type. public IReadOnlyList GetRegistered(Type type) { - Contract.Requires(type != null); + type = type ?? throw new ArgumentNullException(nameof(type)); if (_registeredRoutedEvents.TryGetValue(type, out var events)) { From 4e62ff3ffb9bb68dd13153f357298ac6eeddcf0e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 24 Feb 2020 11:04:20 +0100 Subject: [PATCH 016/298] Added failing test for #3176. --- .../InteractiveTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs index 414e67bb94..0355078a05 100644 --- a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs +++ b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs @@ -358,6 +358,29 @@ namespace Avalonia.Interactivity.UnitTests Assert.Equal(1, called); } + [Fact] + public void Removing_Control_In_Handler_Should_Not_Stop_Event() + { + // Issue #3176 + var ev = new RoutedEvent("test", RoutingStrategies.Bubble, typeof(RoutedEventArgs), typeof(TestInteractive)); + var invoked = new List(); + EventHandler handler = (s, e) => invoked.Add(((TestInteractive)s).Name); + var parent = CreateTree(ev, handler, RoutingStrategies.Bubble | RoutingStrategies.Tunnel); + var target = (IInteractive)parent.GetVisualChildren().Single(); + + EventHandler removeHandler = (s, e) => + { + parent.Children = Array.Empty(); + }; + + target.AddHandler(ev, removeHandler); + + var args = new RoutedEventArgs(ev, target); + target.RaiseEvent(args); + + Assert.Equal(new[] { "3", "2b", "1" }, invoked); + } + private TestInteractive CreateTree( RoutedEvent ev, EventHandler handler, @@ -414,6 +437,7 @@ namespace Avalonia.Interactivity.UnitTests set { + VisualChildren.Clear(); VisualChildren.AddRange(value.Cast()); } } From cca4247c05ce516a904b149ceea972b0ac81dbb4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 24 Feb 2020 10:40:34 +0100 Subject: [PATCH 017/298] Added EventRoute. Instead of traversing the tree while raising an event, instead first build an event route and then raise the event along it. Fixes #3176 --- src/Avalonia.Interactivity/EventRoute.cs | 200 ++++++++++++++++++ .../EventSubscription.cs | 6 +- src/Avalonia.Interactivity/IInteractive.cs | 7 + src/Avalonia.Interactivity/Interactive.cs | 195 +++++------------ src/Avalonia.Interactivity/RoutedEvent.cs | 2 + .../InteractiveTests.cs | 1 - tests/Avalonia.UnitTests/MouseTestHelper.cs | 2 +- 7 files changed, 266 insertions(+), 147 deletions(-) create mode 100644 src/Avalonia.Interactivity/EventRoute.cs diff --git a/src/Avalonia.Interactivity/EventRoute.cs b/src/Avalonia.Interactivity/EventRoute.cs new file mode 100644 index 0000000000..85ba33d7ba --- /dev/null +++ b/src/Avalonia.Interactivity/EventRoute.cs @@ -0,0 +1,200 @@ +using System; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Interactivity +{ + /// + /// Holds the route for a routed event and supports raising an event on that route. + /// + public class EventRoute : IDisposable + { + private readonly RoutedEvent _event; + private PooledList? _route; + + /// + /// Initializes a new instance of the class. + /// + /// The routed event to be raised. + public EventRoute(RoutedEvent e) + { + e = e ?? throw new ArgumentNullException(nameof(e)); + + _event = e; + _route = null; + } + + /// + /// Gets a value indicating whether the route has any handlers. + /// + public bool HasHandlers => _route?.Count > 0; + + /// + /// Adds a handler to the route. + /// + /// The target on which the event should be raised. + /// The handler for the event. + /// The routing strategies to listen to. + /// + /// If true the handler will be raised even when the routed event is marked as handled. + /// + /// + /// An optional adapter which if supplied, will be called with + /// and the parameters for the event. This adapter can be used to avoid calling + /// `DynamicInvoke` on the handler. + /// + public void Add( + IInteractive target, + Delegate handler, + RoutingStrategies routes, + bool handledEventsToo = false, + Action? adapter = null) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + handler = handler ?? throw new ArgumentNullException(nameof(handler)); + + _route ??= new PooledList(16); + _route.Add(new RouteItem(target, handler, adapter, routes, handledEventsToo)); + } + + /// + /// Adds a class handler to the route. + /// + /// The target on which the event should be raised. + public void AddClassHandler(IInteractive target) + { + target = target ?? throw new ArgumentNullException(nameof(target)); + + _route ??= new PooledList(16); + _route.Add(new RouteItem(target, null, null, 0, false)); + } + + /// + /// Raises an event along the route. + /// + /// The event source. + /// The event args. + public void RaiseEvent(IInteractive source, RoutedEventArgs e) + { + source = source ?? throw new ArgumentNullException(nameof(source)); + e = e ?? throw new ArgumentNullException(nameof(e)); + + e.Source = source; + + if (_event.RoutingStrategies == RoutingStrategies.Direct) + { + e.Route = RoutingStrategies.Direct; + RaiseEventImpl(e); + _event.InvokeRouteFinished(e); + } + else + { + if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel)) + { + e.Route = RoutingStrategies.Tunnel; + RaiseEventImpl(e); + _event.InvokeRouteFinished(e); + } + + if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble)) + { + e.Route = RoutingStrategies.Bubble; + RaiseEventImpl(e); + _event.InvokeRouteFinished(e); + } + } + } + + /// + /// Disposes of the event route. + /// + public void Dispose() + { + _route?.Dispose(); + _route = null; + } + + private void RaiseEventImpl(RoutedEventArgs e) + { + if (_route is null) + { + return; + } + + if (e.Source is null) + { + throw new ArgumentException("Event source may not be null", nameof(e)); + } + + IInteractive? lastTarget = null; + var start = 0; + var end = _route.Count; + var step = 1; + + if (e.Route == RoutingStrategies.Tunnel) + { + start = end - 1; + step = end = -1; + } + + for (var i = start; i != end; i += step) + { + var entry = _route[i]; + + // If we've got to a new control then call any RoutedEvent.Raised listeners. + if (entry.Target != lastTarget) + { + if (!e.Handled) + { + _event.InvokeRaised(entry.Target, e); + } + + // If this is a direct event and we've already raised events then we're finished. + if (e.Route == RoutingStrategies.Direct && lastTarget is object) + { + return; + } + + lastTarget = entry.Target; + } + + // Raise the event handler. + if (entry.Handler is object && + entry.Routes.HasFlagCustom(e.Route) && + (!e.Handled || entry.HandledEventsToo)) + { + if (entry.Adapter is object) + { + entry.Adapter(entry.Handler, entry.Target, e); + } + else + { + entry.Handler.DynamicInvoke(entry.Target, e); + } + } + } + } + + private readonly struct RouteItem + { + public RouteItem( + IInteractive target, + Delegate? handler, + Action? adapter, + RoutingStrategies routes, + bool handledEventsToo) + { + Target = target; + Handler = handler; + Adapter = adapter; + Routes = routes; + HandledEventsToo = handledEventsToo; + } + + public IInteractive Target { get; } + public Delegate? Handler { get; } + public Action? Adapter { get; } + public RoutingStrategies Routes { get; } + public bool HandledEventsToo { get; } + } + } +} diff --git a/src/Avalonia.Interactivity/EventSubscription.cs b/src/Avalonia.Interactivity/EventSubscription.cs index d363e3f6fa..50f64f49ee 100644 --- a/src/Avalonia.Interactivity/EventSubscription.cs +++ b/src/Avalonia.Interactivity/EventSubscription.cs @@ -5,15 +5,13 @@ using System; namespace Avalonia.Interactivity { - internal delegate void HandlerInvokeSignature(Delegate baseHandler, object sender, RoutedEventArgs args); - internal class EventSubscription { public EventSubscription( Delegate handler, RoutingStrategies routes, bool handledEventsToo, - HandlerInvokeSignature? invokeAdapter = null) + Action? invokeAdapter = null) { Handler = handler; Routes = routes; @@ -21,7 +19,7 @@ namespace Avalonia.Interactivity InvokeAdapter = invokeAdapter; } - public HandlerInvokeSignature? InvokeAdapter { get; } + public Action? InvokeAdapter { get; } public Delegate Handler { get; } diff --git a/src/Avalonia.Interactivity/IInteractive.cs b/src/Avalonia.Interactivity/IInteractive.cs index 6524794733..33baa9453a 100644 --- a/src/Avalonia.Interactivity/IInteractive.cs +++ b/src/Avalonia.Interactivity/IInteractive.cs @@ -60,6 +60,13 @@ namespace Avalonia.Interactivity void RemoveHandler(RoutedEvent routedEvent, EventHandler handler) where TEventArgs : RoutedEventArgs; + /// + /// Adds the object's handlers for a routed event to an event route. + /// + /// The event. + /// The event route. + void AddToEventRoute(RoutedEvent routedEvent, EventRoute route); + /// /// Raises a routed event. /// diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 0c4649a1ca..5a27192c87 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; using Avalonia.Layout; using Avalonia.VisualTree; @@ -17,15 +15,14 @@ namespace Avalonia.Interactivity { private Dictionary>? _eventHandlers; - private static readonly Dictionary s_invokeHandlerCache = new Dictionary(); + private static readonly Dictionary> s_invokeHandlerCache + = new Dictionary>(); /// /// Gets the interactive parent of the object for bubbling and tunneling events. /// IInteractive? IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; - private Dictionary> EventHandlers => _eventHandlers ??= new Dictionary>(); - /// /// Adds a handler for the specified routed event. /// @@ -130,105 +127,83 @@ namespace Avalonia.Interactivity throw new ArgumentException("Cannot raise an event whose RoutedEvent is null."); } - e.Source ??= this; - - if (e.RoutedEvent.RoutingStrategies == RoutingStrategies.Direct) - { - e.Route = RoutingStrategies.Direct; - RaiseEventImpl(e); - e.RoutedEvent.InvokeRouteFinished(e); - } - - if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Tunnel) != 0) - { - TunnelEvent(e); - e.RoutedEvent.InvokeRouteFinished(e); - } - - if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Bubble) != 0) - { - BubbleEvent(e); - e.RoutedEvent.InvokeRouteFinished(e); - } + using var route = BuildEventRoute(e.RoutedEvent); + route.RaiseEvent(this, e); } - /// - /// Bubbles an event. - /// - /// The event args. - private void BubbleEvent(RoutedEventArgs e) + void IInteractive.AddToEventRoute(RoutedEvent routedEvent, EventRoute route) { - e = e ?? throw new ArgumentNullException(nameof(e)); - - e.Route = RoutingStrategies.Bubble; - - var traverser = HierarchyTraverser.Create(e); - - traverser.Traverse(this); - } - - /// - /// Tunnels an event. - /// - /// The event args. - private void TunnelEvent(RoutedEventArgs e) - { - e = e ?? throw new ArgumentNullException(nameof(e)); - - e.Route = RoutingStrategies.Tunnel; - - var traverser = HierarchyTraverser.Create(e); + routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); + route = route ?? throw new ArgumentNullException(nameof(route)); - traverser.Traverse(this); + if (_eventHandlers != null && + _eventHandlers.TryGetValue(routedEvent, out var subscriptions)) + { + foreach (var sub in subscriptions) + { + route.Add(this, sub.Handler, sub.Routes, sub.HandledEventsToo, sub.InvokeAdapter); + } + } } /// - /// Carries out the actual invocation of an event on this object. + /// Builds an event route for a routed event. /// - /// The event args. - private void RaiseEventImpl(RoutedEventArgs e) + /// The routed event. + /// An describing the route. + /// + /// Usually, calling is sufficent to raise a routed + /// event, however there are situations in which the construction of the event args is expensive + /// and should be avoided if there are no handlers for an event. In these cases you can call + /// this method to build the event route and check the + /// property to see if there are any handlers registered on the route. If there are, call + /// to raise the event. + /// + protected EventRoute BuildEventRoute(RoutedEvent e) { e = e ?? throw new ArgumentNullException(nameof(e)); - e.RoutedEvent!.InvokeRaised(this, e); + var result = new EventRoute(e); + var hasClassHandlers = e.HasRaisedSubscriptions; - if (_eventHandlers is object && - _eventHandlers.TryGetValue(e.RoutedEvent, out var subscriptions) == true) + if (e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble) || + e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel)) { - foreach (var sub in subscriptions.ToList()) - { - bool correctRoute = (e.Route & sub.Routes) != 0; - bool notFinished = !e.Handled || sub.HandledEventsToo; + IInteractive? element = this; - if (correctRoute && notFinished) + while (element != null) + { + if (hasClassHandlers) { - if (sub.InvokeAdapter != null) - { - sub.InvokeAdapter(sub.Handler, this, e); - } - else - { - sub.Handler.DynamicInvoke(this, e); - } + result.AddClassHandler(element); } + + element.AddToEventRoute(e, result); + element = element.InteractiveParent; } } - } - - private List GetEventSubscriptions(RoutedEvent routedEvent) - { - if (!EventHandlers.TryGetValue(routedEvent, out var subscriptions)) + else { - subscriptions = new List(); - EventHandlers.Add(routedEvent, subscriptions); + if (hasClassHandlers) + { + result.AddClassHandler(this); + } + + ((IInteractive)this).AddToEventRoute(e, result); } - return subscriptions; + return result; } private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription) { - List subscriptions = GetEventSubscriptions(routedEvent); + _eventHandlers ??= new Dictionary>(); + + if (!_eventHandlers.TryGetValue(routedEvent, out var subscriptions)) + { + subscriptions = new List(); + _eventHandlers.Add(routedEvent, subscriptions); + } subscriptions.Add(subscription); @@ -251,67 +226,5 @@ namespace Avalonia.Interactivity _subscriptions.Remove(_subscription); } } - - private interface ITraverse - { - void Execute(IInteractive target, RoutedEventArgs e); - } - - private struct NopTraverse : ITraverse - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Execute(IInteractive target, RoutedEventArgs e) - { - } - } - - private struct RaiseEventTraverse : ITraverse - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Execute(IInteractive target, RoutedEventArgs e) - { - ((Interactive)target).RaiseEventImpl(e); - } - } - - /// - /// Traverses interactive hierarchy allowing for raising events. - /// - /// Called before parent is traversed. - /// Called after parent has been traversed. - private struct HierarchyTraverser - where TPreTraverse : struct, ITraverse - where TPostTraverse : struct, ITraverse - { - private TPreTraverse _preTraverse; - private TPostTraverse _postTraverse; - private readonly RoutedEventArgs _args; - - private HierarchyTraverser(TPreTraverse preTraverse, TPostTraverse postTraverse, RoutedEventArgs args) - { - _preTraverse = preTraverse; - _postTraverse = postTraverse; - _args = args; - } - - public static HierarchyTraverser Create(RoutedEventArgs args) - { - return new HierarchyTraverser(new TPreTraverse(), new TPostTraverse(), args); - } - - public void Traverse(IInteractive target) - { - _preTraverse.Execute(target, _args); - - var parent = target.InteractiveParent; - - if (parent != null) - { - Traverse(parent); - } - - _postTraverse.Execute(target, _args); - } - } } } diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Interactivity/RoutedEvent.cs index 164a86fab7..e515efd3b4 100644 --- a/src/Avalonia.Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Interactivity/RoutedEvent.cs @@ -48,6 +48,8 @@ namespace Avalonia.Interactivity public RoutingStrategies RoutingStrategies { get; } + public bool HasRaisedSubscriptions => _raised.HasObservers; + public IObservable<(object, RoutedEventArgs)> Raised => _raised; public IObservable RouteFinished => _routeFinished; diff --git a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs index 0355078a05..ef3770d1d9 100644 --- a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs +++ b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Interactivity; using Avalonia.VisualTree; using Xunit; diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index f6454a9cd2..bf75b40a72 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -56,7 +56,7 @@ namespace Avalonia.UnitTests { _pressedButton = mouseButton; _pointer.Capture((IInputElement)target); - target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props, + source.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props, GetModifiers(modifiers), clickCount)); } } From 0f7e3e1b8286c15aa85eaa56cbb8cce1e86b989c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 24 Feb 2020 11:09:24 +0100 Subject: [PATCH 018/298] Make EventSubscription a private class. --- .../EventSubscription.cs | 30 ------------------- src/Avalonia.Interactivity/Interactive.cs | 23 ++++++++++++++ 2 files changed, 23 insertions(+), 30 deletions(-) delete mode 100644 src/Avalonia.Interactivity/EventSubscription.cs diff --git a/src/Avalonia.Interactivity/EventSubscription.cs b/src/Avalonia.Interactivity/EventSubscription.cs deleted file mode 100644 index 50f64f49ee..0000000000 --- a/src/Avalonia.Interactivity/EventSubscription.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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; - -namespace Avalonia.Interactivity -{ - internal class EventSubscription - { - public EventSubscription( - Delegate handler, - RoutingStrategies routes, - bool handledEventsToo, - Action? invokeAdapter = null) - { - Handler = handler; - Routes = routes; - HandledEventsToo = handledEventsToo; - InvokeAdapter = invokeAdapter; - } - - public Action? InvokeAdapter { get; } - - public Delegate Handler { get; } - - public RoutingStrategies Routes { get; } - - public bool HandledEventsToo { get; } - } -} diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 5a27192c87..6992ebcf34 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -210,6 +210,29 @@ namespace Avalonia.Interactivity return new UnsubscribeDisposable(subscriptions, subscription); } + private sealed class EventSubscription + { + public EventSubscription( + Delegate handler, + RoutingStrategies routes, + bool handledEventsToo, + Action? invokeAdapter = null) + { + Handler = handler; + Routes = routes; + HandledEventsToo = handledEventsToo; + InvokeAdapter = invokeAdapter; + } + + public Action? InvokeAdapter { get; } + + public Delegate Handler { get; } + + public RoutingStrategies Routes { get; } + + public bool HandledEventsToo { get; } + } + private sealed class UnsubscribeDisposable : IDisposable { private readonly List _subscriptions; From 2e99fa9a91607abf41e6d82dbcb939917bb1edf7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Feb 2020 10:29:16 +0100 Subject: [PATCH 019/298] Make setting styled values disposable. --- src/Avalonia.Base/AvaloniaObject.cs | 9 +++-- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 17 +++++---- src/Avalonia.Base/AvaloniaProperty.cs | 5 ++- src/Avalonia.Base/DirectPropertyBase.cs | 4 ++- src/Avalonia.Base/IAvaloniaObject.cs | 2 +- .../PropertyStore/ConstantValueEntry.cs | 11 ++++-- .../PropertyStore/PriorityValue.cs | 9 +++-- src/Avalonia.Base/StyledPropertyBase.cs | 6 ++-- src/Avalonia.Base/ValueStore.cs | 25 ++++++++----- .../AvaloniaObjectTests_SetValue.cs | 35 +++++++++++++++++++ .../AvaloniaPropertyTests.cs | 2 +- .../PriorityValueTests.cs | 6 +++- 12 files changed, 103 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index b0ff591682..88b99cd99a 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -311,7 +311,10 @@ namespace Avalonia /// The property. /// The value. /// The priority of the value. - public void SetValue( + /// + /// An if setting the property can be undone, otherwise null. + /// + public IDisposable SetValue( StyledPropertyBase property, T value, BindingPriority priority = BindingPriority.LocalValue) @@ -335,8 +338,10 @@ namespace Avalonia } else if (!(value is DoNothingType)) { - Values.SetValue(property, value, priority); + return Values.SetValue(property, value, priority); } + + return null; } /// diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index a4c7fa95a5..0f82042dcd 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -458,7 +458,10 @@ namespace Avalonia /// The property. /// The value. /// The priority of the value. - public static void SetValue( + /// + /// An if setting the property can be undone, otherwise null. + /// + public static IDisposable SetValue( this IAvaloniaObject target, AvaloniaProperty property, object value, @@ -467,7 +470,7 @@ namespace Avalonia target = target ?? throw new ArgumentNullException(nameof(target)); property = property ?? throw new ArgumentNullException(nameof(property)); - property.RouteSetValue(target, value, priority); + return property.RouteSetValue(target, value, priority); } /// @@ -478,7 +481,10 @@ namespace Avalonia /// The property. /// The value. /// The priority of the value. - public static void SetValue( + /// + /// An if setting the property can be undone, otherwise null. + /// + public static IDisposable SetValue( this IAvaloniaObject target, AvaloniaProperty property, T value, @@ -490,11 +496,10 @@ namespace Avalonia switch (property) { case StyledPropertyBase styled: - target.SetValue(styled, value, priority); - break; + return target.SetValue(styled, value, priority); case DirectPropertyBase direct: target.SetValue(direct, value); - break; + return null; default: throw new NotSupportedException("Unsupported AvaloniaProperty type."); } diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index aa7a675764..b0858f8bc7 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -496,7 +496,10 @@ namespace Avalonia /// The object instance. /// The value. /// The priority. - internal abstract void RouteSetValue( + /// + /// An if setting the property can be undone, otherwise null. + /// + internal abstract IDisposable? RouteSetValue( IAvaloniaObject o, object value, BindingPriority priority); diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 39ed3b084f..d0dd841a70 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -114,7 +114,7 @@ namespace Avalonia } /// - internal override void RouteSetValue( + internal override IDisposable? RouteSetValue( IAvaloniaObject o, object value, BindingPriority priority) @@ -133,6 +133,8 @@ namespace Avalonia { throw v.Error!; } + + return null; } /// diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index fb85ae222c..81a212b087 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -65,7 +65,7 @@ namespace Avalonia /// The property. /// The value. /// The priority of the value. - void SetValue( + IDisposable SetValue( StyledPropertyBase property, T value, BindingPriority priority = BindingPriority.LocalValue); diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs index f15f56e32b..aa054c46ff 100644 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -10,16 +10,20 @@ namespace Avalonia.PropertyStore /// . /// /// The property type. - internal class ConstantValueEntry : IPriorityValueEntry + internal class ConstantValueEntry : IPriorityValueEntry, IDisposable { + private IValueSink _sink; + public ConstantValueEntry( StyledPropertyBase property, T value, - BindingPriority priority) + BindingPriority priority, + IValueSink sink) { Property = property; Value = value; Priority = priority; + _sink = sink; } public StyledPropertyBase Property { get; } @@ -28,6 +32,7 @@ namespace Avalonia.PropertyStore Optional IValue.Value => Value.ToObject(); BindingPriority IValue.ValuePriority => Priority; - public void Reparent(IValueSink sink) { } + public void Dispose() => _sink.Completed(Property, this, Value); + public void Reparent(IValueSink sink) => _sink = sink; } } diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 4ef8f650fa..affb20f334 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -78,8 +78,10 @@ namespace Avalonia.PropertyStore public void ClearLocalValue() => UpdateEffectiveValue(); - public void SetValue(T value, BindingPriority priority) + public IDisposable? SetValue(T value, BindingPriority priority) { + IDisposable? result = null; + if (priority == BindingPriority.LocalValue) { _localValue = value; @@ -87,10 +89,13 @@ namespace Avalonia.PropertyStore else { var insert = FindInsertPoint(priority); - _entries.Insert(insert, new ConstantValueEntry(Property, value, priority)); + var entry = new ConstantValueEntry(Property, value, priority, this); + _entries.Insert(insert, entry); + result = entry; } UpdateEffectiveValue(); + return result; } public BindingEntry AddBinding(IObservable> source, BindingPriority priority) diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index d1f961a567..53fcb51c5b 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -194,7 +194,7 @@ namespace Avalonia } /// - internal override void RouteSetValue( + internal override IDisposable RouteSetValue( IAvaloniaObject o, object value, BindingPriority priority) @@ -203,7 +203,7 @@ namespace Avalonia if (v.HasValue) { - o.SetValue(this, (TValue)v.Value, priority); + return o.SetValue(this, (TValue)v.Value, priority); } else if (v.Type == BindingValueType.UnsetValue) { @@ -213,6 +213,8 @@ namespace Avalonia { throw v.Error; } + + return null; } /// diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index e310be0f0a..104c06de0f 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -70,23 +70,25 @@ namespace Avalonia return false; } - public void SetValue(StyledPropertyBase property, T value, BindingPriority priority) + public IDisposable? SetValue(StyledPropertyBase property, T value, BindingPriority priority) { if (property.ValidateValue?.Invoke(value) == false) { throw new ArgumentException($"{value} is not a valid value for '{property.Name}."); } + IDisposable? result = null; + if (_values.TryGetValue(property, out var slot)) { - SetExisting(slot, property, value, priority); + result = SetExisting(slot, property, value, priority); } else if (property.HasCoercion) { // If the property has any coercion callbacks then always create a PriorityValue. var entry = new PriorityValue(_owner, property, this); _values.AddValue(property, entry); - entry.SetValue(value, priority); + result = entry.SetValue(value, priority); } else if (priority == BindingPriority.LocalValue) { @@ -95,10 +97,13 @@ namespace Avalonia } else { - var entry = new ConstantValueEntry(property, value, priority); + var entry = new ConstantValueEntry(property, value, priority, this); _values.AddValue(property, entry); _sink.ValueChanged(property, priority, default, value); + result = entry; } + + return result; } public IDisposable AddBinding( @@ -205,21 +210,23 @@ namespace Avalonia } } - private void SetExisting( + private IDisposable? SetExisting( object slot, StyledPropertyBase property, T value, BindingPriority priority) { + IDisposable? result = null; + if (slot is IPriorityValueEntry e) { var priorityValue = new PriorityValue(_owner, property, this, e); _values.SetValue(property, priorityValue); - priorityValue.SetValue(value, priority); + result = priorityValue.SetValue(value, priority); } else if (slot is PriorityValue p) { - p.SetValue(value, priority); + result = p.SetValue(value, priority); } else if (slot is LocalValueEntry l) { @@ -232,7 +239,7 @@ namespace Avalonia else { var priorityValue = new PriorityValue(_owner, property, this, l); - priorityValue.SetValue(value, priority); + result = priorityValue.SetValue(value, priority); _values.SetValue(property, priorityValue); } } @@ -240,6 +247,8 @@ namespace Avalonia { throw new NotSupportedException("Unrecognised value store slot type."); } + + return result; } private IDisposable BindExisting( diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs index 4b477287e8..1b8cd787f2 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs @@ -284,6 +284,41 @@ namespace Avalonia.Base.UnitTests Assert.Equal("newvalue", target.GetValue(Class1.FrankProperty)); } + [Fact] + public void Disposing_Style_SetValue_Reverts_To_DefaultValue() + { + Class1 target = new Class1(); + + var d = target.SetValue(Class1.FooProperty, "foo", BindingPriority.Style); + d.Dispose(); + + Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Disposing_Style_SetValue_Reverts_To_Previous_Style_Value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "foo", BindingPriority.Style); + var d = target.SetValue(Class1.FooProperty, "bar", BindingPriority.Style); + d.Dispose(); + + Assert.Equal("foo", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Disposing_Animation_SetValue_Reverts_To_Previous_Local_Value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "foo", BindingPriority.LocalValue); + var d = target.SetValue(Class1.FooProperty, "bar", BindingPriority.Animation); + d.Dispose(); + + Assert.Equal("foo", target.GetValue(Class1.FooProperty)); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 6bb8dfe1f5..788376b2fd 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -146,7 +146,7 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } - internal override void RouteSetValue( + internal override IDisposable RouteSetValue( IAvaloniaObject o, object value, BindingPriority priority) diff --git a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs index 8c76445645..5e69b8490d 100644 --- a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs +++ b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs @@ -24,7 +24,11 @@ namespace Avalonia.Base.UnitTests Owner, TestProperty, NullSink, - new ConstantValueEntry(TestProperty, "1", BindingPriority.StyleTrigger)); + new ConstantValueEntry( + TestProperty, + "1", + BindingPriority.StyleTrigger, + NullSink)); Assert.Equal("1", target.Value.Value); Assert.Equal(BindingPriority.StyleTrigger, target.ValuePriority); From b82ca9aca987d6ba62269743e61bbd0ab1ad77ba Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Feb 2020 11:17:06 +0100 Subject: [PATCH 020/298] Add an AvaloniaProperty visitor. We already have some specific internal methods for routing certain methods via an untyped property to a typed property, but adding support for the visitor pattern allows us to support arbitrary use-cases. --- src/Avalonia.Base/AvaloniaProperty.cs | 9 +++++ src/Avalonia.Base/DirectPropertyBase.cs | 7 ++++ src/Avalonia.Base/StyledPropertyBase.cs | 7 ++++ .../Utilities/IAvaloniaPropertyVisitor.cs | 34 +++++++++++++++++++ .../AvaloniaPropertyTests.cs | 6 ++++ 5 files changed, 63 insertions(+) create mode 100644 src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index b0858f8bc7..394e22eac1 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -469,6 +469,15 @@ namespace Avalonia return Name; } + /// + /// Uses the visitor pattern to resolve an untyped property to a typed property. + /// + /// The type of user data passed. + /// The visitor which will accept the typed property. + /// The user data to pass. + public abstract void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + where TData : struct; + /// /// Notifies the observable. /// diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index d0dd841a70..d3b5277c53 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Data; using Avalonia.Reactive; +using Avalonia.Utilities; #nullable enable @@ -101,6 +102,12 @@ namespace Avalonia return (DirectPropertyMetadata)base.GetMetadata(type); } + /// + public override void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + { + vistor.Visit(this, ref data); + } + /// internal override void RouteClearValue(IAvaloniaObject o) { diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 53fcb51c5b..fb07ef3f62 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using Avalonia.Data; using Avalonia.Reactive; +using Avalonia.Utilities; namespace Avalonia { @@ -169,6 +170,12 @@ namespace Avalonia base.OverrideMetadata(type, metadata); } + /// + public override void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + { + vistor.Visit(this, ref data); + } + /// /// Gets the string representation of the property. /// diff --git a/src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs b/src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs new file mode 100644 index 0000000000..4b889eb129 --- /dev/null +++ b/src/Avalonia.Base/Utilities/IAvaloniaPropertyVisitor.cs @@ -0,0 +1,34 @@ +#nullable enable + +namespace Avalonia.Utilities +{ + /// + /// A visitor to resolve an untyped to a typed property. + /// + /// The type of user data passed. + /// + /// Pass an instance that implements this interface to + /// + /// in order to resolve un untyped to a typed + /// or . + /// + public interface IAvaloniaPropertyVisitor + where TData : struct + { + /// + /// Called when the property is a styled property. + /// + /// The property value type. + /// The property. + /// The user data. + void Visit(StyledPropertyBase property, ref TData data); + + /// + /// Called when the property is a direct property. + /// + /// The property value type. + /// The property. + /// The user data. + void Visit(DirectPropertyBase property, ref TData data); + } +} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 788376b2fd..c7eebdd70a 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Data; +using Avalonia.Utilities; using Xunit; namespace Avalonia.Base.UnitTests @@ -123,6 +124,11 @@ namespace Avalonia.Base.UnitTests OverrideMetadata(typeof(T), metadata); } + public override void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + { + throw new NotImplementedException(); + } + internal override IDisposable RouteBind( IAvaloniaObject o, IObservable> source, From dc55d6528775ddd8ccc7b31257c02ba3f0c7fcd8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Feb 2020 17:52:06 +0100 Subject: [PATCH 021/298] Refactored styling. - Don't use Rx in the styling system. Instead introduces `IStyleActivator` which is like an `IObservable`-lite in order to cut down on allocations. - #nullable enable on touched files --- .../Diagnostics/ViewModels/TreeNode.cs | 17 +- src/Avalonia.Styling/Avalonia.Styling.csproj | 1 - .../Controls/NameScopeLocator.cs | 6 - src/Avalonia.Styling/StyledElement.cs | 138 +++++++------- .../Styling/ActivatedObservable.cs | 77 -------- .../Styling/ActivatedSubject.cs | 110 ------------ .../Styling/ActivatedValue.cs | 133 -------------- .../Styling/Activators/AndActivator.cs | 67 +++++++ .../Styling/Activators/IStyleActivator.cs | 33 ++++ .../Styling/Activators/IStyleActivatorSink.cs | 17 ++ .../Styling/Activators/NotActivator.cs | 13 ++ .../Styling/Activators/OrActivator.cs | 67 +++++++ .../Activators/PropertyEqualsActivator.cs | 35 ++++ .../Styling/Activators/StyleActivatorBase.cs | 55 ++++++ .../Styling/Activators/StyleClassActivator.cs | 72 ++++++++ .../Styling/DescendentSelector.cs | 48 +++-- src/Avalonia.Styling/Styling/ISetter.cs | 19 +- .../Styling/ISetterInstance.cs | 20 +++ src/Avalonia.Styling/Styling/IStyle.cs | 16 +- .../Styling/IStyleInstance.cs | 22 +++ src/Avalonia.Styling/Styling/IStyleable.cs | 20 ++- src/Avalonia.Styling/Styling/NotSelector.cs | 16 +- src/Avalonia.Styling/Styling/OrSelector.cs | 53 ++++-- .../Styling/PropertyEqualsSelector.cs | 25 ++- .../Styling/PropertySetterBindingInstance.cs | 48 +++++ .../Styling/PropertySetterInstance.cs | 82 +++++++++ src/Avalonia.Styling/Styling/Selector.cs | 44 +++-- src/Avalonia.Styling/Styling/SelectorMatch.cs | 29 +-- src/Avalonia.Styling/Styling/Setter.cs | 141 ++++++--------- src/Avalonia.Styling/Styling/Style.cs | 165 +++-------------- .../Styling/StyleActivator.cs | 56 ------ src/Avalonia.Styling/Styling/StyleInstance.cs | 81 +++++++++ src/Avalonia.Styling/Styling/Styler.cs | 25 ++- src/Avalonia.Styling/Styling/Styles.cs | 82 ++++----- .../Styling/TypeNameAndClassSelector.cs | 109 ++--------- .../Styling/StyleInclude.cs | 19 +- .../Styling/ApplyStyling.cs | 2 +- .../Styling/StyleAttachBenchmark.cs | 6 +- .../Primitives/TemplatedControlTests.cs | 8 +- .../TabControlTests.cs | 2 +- .../UserControlTests.cs | 2 +- .../AvaloniaPropertyConverterTest.cs | 10 +- .../StyleTests.cs | 37 +--- .../ActivatedObservableTests.cs | 71 -------- .../ActivatedSubjectTests.cs | 92 ---------- .../ActivatedValueTests.cs | 75 -------- .../SelectorTests_Class.cs | 16 +- .../SelectorTests_Descendent.cs | 2 +- .../SelectorTests_Multiple.cs | 29 +++ .../SelectorTests_Not.cs | 8 +- .../SelectorTests_PropertyEquals.cs | 2 +- .../Avalonia.Styling.UnitTests/SetterTests.cs | 51 +++--- .../StyleActivatorExtensions.cs | 42 +++++ .../StyleActivatorTests.cs | 169 ------------------ .../Avalonia.Styling.UnitTests/StyleTests.cs | 58 ++---- .../StyledElementTests.cs | 9 +- 56 files changed, 1166 insertions(+), 1486 deletions(-) delete mode 100644 src/Avalonia.Styling/Styling/ActivatedObservable.cs delete mode 100644 src/Avalonia.Styling/Styling/ActivatedSubject.cs delete mode 100644 src/Avalonia.Styling/Styling/ActivatedValue.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/AndActivator.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/NotActivator.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/OrActivator.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs create mode 100644 src/Avalonia.Styling/Styling/ISetterInstance.cs create mode 100644 src/Avalonia.Styling/Styling/IStyleInstance.cs create mode 100644 src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs create mode 100644 src/Avalonia.Styling/Styling/PropertySetterInstance.cs delete mode 100644 src/Avalonia.Styling/Styling/StyleActivator.cs create mode 100644 src/Avalonia.Styling/Styling/StyleInstance.cs delete mode 100644 tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs delete mode 100644 tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs delete mode 100644 tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs create mode 100644 tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs delete mode 100644 tests/Avalonia.Styling.UnitTests/StyleActivatorTests.cs diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 7c403e1b04..4eca8a3c25 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -6,6 +6,8 @@ using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.VisualTree; @@ -22,22 +24,25 @@ namespace Avalonia.Diagnostics.ViewModels Type = visual.GetType().Name; Visual = visual; - if (visual is IStyleable styleable) + if (visual is IControl control) { + var removed = Observable.FromEventPattern( + x => control.DetachedFromLogicalTree += x, + x => control.DetachedFromLogicalTree -= x); var classesChanged = Observable.FromEventPattern< NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>( - x => styleable.Classes.CollectionChanged += x, - x => styleable.Classes.CollectionChanged -= x) - .TakeUntil(((IStyleable)styleable).StyleDetach); + x => control.Classes.CollectionChanged += x, + x => control.Classes.CollectionChanged -= x) + .TakeUntil(removed); classesChanged.Select(_ => Unit.Default) .StartWith(Unit.Default) .Subscribe(_ => { - if (styleable.Classes.Count > 0) + if (control.Classes.Count > 0) { - Classes = "(" + string.Join(" ", styleable.Classes) + ")"; + Classes = "(" + string.Join(" ", control.Classes) + ")"; } else { diff --git a/src/Avalonia.Styling/Avalonia.Styling.csproj b/src/Avalonia.Styling/Avalonia.Styling.csproj index a396cee35f..b4f6c2c942 100644 --- a/src/Avalonia.Styling/Avalonia.Styling.csproj +++ b/src/Avalonia.Styling/Avalonia.Styling.csproj @@ -8,5 +8,4 @@ - diff --git a/src/Avalonia.Styling/Controls/NameScopeLocator.cs b/src/Avalonia.Styling/Controls/NameScopeLocator.cs index 354ed33657..51f4c5c4eb 100644 --- a/src/Avalonia.Styling/Controls/NameScopeLocator.cs +++ b/src/Avalonia.Styling/Controls/NameScopeLocator.cs @@ -1,11 +1,5 @@ using System; -using System.Linq; using System.Reactive.Disposables; -using System.Reactive.Threading.Tasks; -using System.Reflection; -using System.Threading.Tasks; -using Avalonia.LogicalTree; -using Avalonia.Reactive; using Avalonia.Utilities; namespace Avalonia.Controls diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 120a53c664..aeb3b5dc53 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; -using System.Reactive.Subjects; using Avalonia.Animation; using Avalonia.Collections; using Avalonia.Controls; @@ -14,6 +12,8 @@ using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.Styling; +#nullable enable + namespace Avalonia { /// @@ -29,8 +29,8 @@ namespace Avalonia /// /// Defines the property. /// - public static readonly StyledProperty DataContextProperty = - AvaloniaProperty.Register( + public static readonly StyledProperty DataContextProperty = + AvaloniaProperty.Register( nameof(DataContext), inherits: true, notifying: DataContextNotifying); @@ -38,34 +38,34 @@ namespace Avalonia /// /// Defines the property. /// - public static readonly DirectProperty NameProperty = - AvaloniaProperty.RegisterDirect(nameof(Name), o => o.Name, (o, v) => o.Name = v); + public static readonly DirectProperty NameProperty = + AvaloniaProperty.RegisterDirect(nameof(Name), o => o.Name, (o, v) => o.Name = v); /// /// Defines the property. /// - public static readonly DirectProperty ParentProperty = - AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); + public static readonly DirectProperty ParentProperty = + AvaloniaProperty.RegisterDirect(nameof(Parent), o => o.Parent); /// /// Defines the property. /// - public static readonly DirectProperty TemplatedParentProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty TemplatedParentProperty = + AvaloniaProperty.RegisterDirect( nameof(TemplatedParent), o => o.TemplatedParent, (o ,v) => o.TemplatedParent = v); private int _initCount; - private string _name; + private string? _name; private readonly Classes _classes = new Classes(); - private ILogicalRoot _logicalRoot; - private IAvaloniaList _logicalChildren; - private IResourceDictionary _resources; - private Styles _styles; + private ILogicalRoot? _logicalRoot; + private IAvaloniaList? _logicalChildren; + private IResourceDictionary? _resources; + private Styles? _styles; private bool _styled; - private Subject _styleDetach = new Subject(); - private ITemplatedControl _templatedParent; + private List? _appliedStyles; + private ITemplatedControl? _templatedParent; private bool _dataContextUpdating; /// @@ -87,12 +87,12 @@ namespace Avalonia /// /// Raised when the styled element is attached to a rooted logical tree. /// - public event EventHandler AttachedToLogicalTree; + public event EventHandler? AttachedToLogicalTree; /// /// Raised when the styled element is detached from a rooted logical tree. /// - public event EventHandler DetachedFromLogicalTree; + public event EventHandler? DetachedFromLogicalTree; /// /// Occurs when the property changes. @@ -101,7 +101,7 @@ namespace Avalonia /// This event will be raised when the property has changed and /// all subscribers to that change have been notified. /// - public event EventHandler DataContextChanged; + public event EventHandler? DataContextChanged; /// /// Occurs when the styled element has finished initialization. @@ -114,12 +114,12 @@ namespace Avalonia /// is not used, it is called when the styled element is attached /// to the visual tree. /// - public event EventHandler Initialized; + public event EventHandler? Initialized; /// /// Occurs when a resource in this styled element or a parent styled element has changed. /// - public event EventHandler ResourcesChanged; + public event EventHandler? ResourcesChanged; /// /// Gets or sets the name of the styled element. @@ -128,20 +128,12 @@ namespace Avalonia /// An element's name is used to uniquely identify an element within the element's name /// scope. Once the element is added to a logical tree, its name cannot be changed. /// - public string Name + public string? Name { - get - { - return _name; - } + get => _name; set { - if (String.IsNullOrWhiteSpace(value)) - { - throw new InvalidOperationException("Cannot set Name to null or empty string."); - } - if (_styled) { throw new InvalidOperationException("Cannot set Name : styled element already styled."); @@ -189,7 +181,7 @@ namespace Avalonia /// The data context is an inherited property that specifies the default object that will /// be used for data binding. /// - public object DataContext + public object? DataContext { get { return GetValue(DataContextProperty); } set { SetValue(DataContextProperty, value); } @@ -214,28 +206,15 @@ namespace Avalonia /// public Styles Styles { - get { return _styles ?? (Styles = new Styles()); } - set + get { - Contract.Requires(value != null); - - if (_styles != value) + if (_styles is null) { - if (_styles != null) - { - (_styles as ISetResourceParent)?.SetParent(null); - _styles.ResourcesChanged -= ThisResourcesChanged; - } - - _styles = value; - - if (value is ISetResourceParent setParent && setParent.ResourceParent == null) - { - setParent.SetParent(this); - } - + _styles = new Styles(this); _styles.ResourcesChanged += ThisResourcesChanged; } + + return _styles; } } @@ -247,7 +226,7 @@ namespace Avalonia get => _resources ?? (Resources = new ResourceDictionary()); set { - Contract.Requires(value != null); + value = value ?? throw new ArgumentNullException(nameof(value)); var hadResources = false; @@ -270,7 +249,7 @@ namespace Avalonia /// /// Gets the styled element whose lookless template this styled element is part of. /// - public ITemplatedControl TemplatedParent + public ITemplatedControl? TemplatedParent { get => _templatedParent; internal set => SetAndRaise(TemplatedParentProperty, ref _templatedParent, value); @@ -312,12 +291,12 @@ namespace Avalonia /// /// Gets the styled element's logical parent. /// - public IStyledElement Parent { get; private set; } + public IStyledElement? Parent { get; private set; } /// /// Gets the styled element's logical parent. /// - ILogical ILogical.LogicalParent => Parent; + ILogical? ILogical.LogicalParent => Parent; /// /// Gets the styled element's logical children. @@ -328,7 +307,7 @@ namespace Avalonia bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources; /// - IResourceNode IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode; + IResourceNode? IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode; /// IAvaloniaReadOnlyList IStyleable.Classes => Classes; @@ -344,14 +323,11 @@ namespace Avalonia /// Type IStyleable.StyleKey => GetType(); - /// - IObservable IStyleable.StyleDetach => _styleDetach; - /// bool IStyleHost.IsStylesInitialized => _styles != null; /// - IStyleHost IStyleHost.StylingParent => (IStyleHost)InheritanceParent; + IStyleHost? IStyleHost.StylingParent => (IStyleHost)InheritanceParent; /// public virtual void BeginInit() @@ -397,13 +373,13 @@ namespace Avalonia /// void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { - this.OnAttachedToLogicalTreeCore(e); + OnAttachedToLogicalTreeCore(e); } /// void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { - this.OnDetachedFromLogicalTreeCore(e); + OnDetachedFromLogicalTreeCore(e); } /// @@ -413,7 +389,7 @@ namespace Avalonia } /// - bool IResourceProvider.TryGetResource(object key, out object value) + bool IResourceProvider.TryGetResource(object key, out object? value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || @@ -424,7 +400,7 @@ namespace Avalonia /// Sets the styled element's logical parent. /// /// The parent. - void ISetLogicalParent.SetParent(ILogical parent) + void ISetLogicalParent.SetParent(ILogical? parent) { var old = Parent; @@ -440,7 +416,7 @@ namespace Avalonia InheritanceParent = parent as AvaloniaObject; } - Parent = (IStyledElement)parent; + Parent = (IStyledElement?)parent; if (_logicalRoot != null) { @@ -470,12 +446,13 @@ namespace Avalonia var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent); OnAttachedToLogicalTreeCore(e); } - +#nullable disable RaisePropertyChanged( ParentProperty, new Optional(old), new BindingValue(Parent), BindingPriority.LocalValue); +#nullable enable } } @@ -488,6 +465,16 @@ namespace Avalonia InheritanceParent = parent; } + void IStyleable.StyleApplied(IStyleInstance instance) + { + instance = instance ?? throw new ArgumentNullException(nameof(instance)); + + _appliedStyles ??= new List(); + _appliedStyles.Add(instance); + } + + void IStyleable.DetachStyles() => DetachStyles(); + protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -597,7 +584,7 @@ namespace Avalonia } } - private static ILogicalRoot FindLogicalRoot(IStyleHost e) + private static ILogicalRoot? FindLogicalRoot(IStyleHost e) { while (e != null) { @@ -666,7 +653,7 @@ namespace Avalonia if (_logicalRoot != null) { _logicalRoot = null; - _styleDetach.OnNext(this); + DetachStyles(); OnDetachedFromLogicalTree(e); DetachedFromLogicalTree?.Invoke(this, e); @@ -682,7 +669,7 @@ namespace Avalonia } #if DEBUG - if (((INotifyCollectionChangedDebug)_classes).GetCollectionChangedSubscribers()?.Length > 0) + if (((INotifyCollectionChangedDebug)Classes).GetCollectionChangedSubscribers()?.Length > 0) { Logger.TryGet(LogEventLevel.Warning)?.Log( LogArea.Control, @@ -710,6 +697,19 @@ namespace Avalonia } } + private void DetachStyles() + { + if (_appliedStyles is object) + { + foreach (var i in _appliedStyles) + { + i.Dispose(); + } + + _appliedStyles.Clear(); + } + } + private void ClearLogicalParent(IEnumerable children) { foreach (var i in children) diff --git a/src/Avalonia.Styling/Styling/ActivatedObservable.cs b/src/Avalonia.Styling/Styling/ActivatedObservable.cs deleted file mode 100644 index 5b2774943a..0000000000 --- a/src/Avalonia.Styling/Styling/ActivatedObservable.cs +++ /dev/null @@ -1,77 +0,0 @@ -// 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; - -namespace Avalonia.Styling -{ - /// - /// An observable which is switched on or off according to an activator observable. - /// - /// - /// An has two inputs: an activator observable and a - /// observable which produces the activated value. When the activator - /// produces true, the will produce the current activated - /// value. When the activator produces false it will produce - /// . - /// - internal class ActivatedObservable : ActivatedValue, IDescription - { - private IDisposable _sourceSubscription; - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// An observable that produces the activated value. - /// The binding description. - public ActivatedObservable( - IObservable activator, - IObservable source, - string description) - : base(activator, AvaloniaProperty.UnsetValue, description) - { - Contract.Requires(source != null); - - Source = source; - } - - /// - /// Gets an observable which produces the . - /// - public IObservable Source { get; } - - protected override ActivatorListener CreateListener() => new ValueListener(this); - - protected override void Deinitialize() - { - base.Deinitialize(); - _sourceSubscription.Dispose(); - _sourceSubscription = null; - } - - protected override void Initialize() - { - base.Initialize(); - _sourceSubscription = Source.Subscribe((ValueListener)Listener); - } - - protected virtual void NotifyValue(object value) - { - Value = value; - } - - private class ValueListener : ActivatorListener, IObserver - { - public ValueListener(ActivatedObservable parent) - : base(parent) - { - } - protected new ActivatedObservable Parent => (ActivatedObservable)base.Parent; - - void IObserver.OnCompleted() => Parent.CompletedReceived(); - void IObserver.OnError(Exception error) => Parent.ErrorReceived(error); - void IObserver.OnNext(object value) => Parent.NotifyValue(value); - } - } -} diff --git a/src/Avalonia.Styling/Styling/ActivatedSubject.cs b/src/Avalonia.Styling/Styling/ActivatedSubject.cs deleted file mode 100644 index a8446c4bfb..0000000000 --- a/src/Avalonia.Styling/Styling/ActivatedSubject.cs +++ /dev/null @@ -1,110 +0,0 @@ -// 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 System.Reactive.Subjects; - -namespace Avalonia.Styling -{ - /// - /// A subject which is switched on or off according to an activator observable. - /// - /// - /// An extends to - /// be an . When the object is active then values - /// received via will be passed to the source subject. - /// - internal class ActivatedSubject : ActivatedObservable, ISubject, IDescription - { - private bool _completed; - private object _pushValue; - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// An observable that produces the activated value. - /// The binding description. - public ActivatedSubject( - IObservable activator, - ISubject source, - string description) - : base(activator, source, description) - { - } - - /// - /// Gets the underlying subject. - /// - public new ISubject Source - { - get { return (ISubject)base.Source; } - } - - public void OnCompleted() - { - Source.OnCompleted(); - } - - public void OnError(Exception error) - { - Source.OnError(error); - } - - public void OnNext(object value) - { - _pushValue = value; - - if (IsActive == true && !_completed) - { - Source.OnNext(_pushValue); - } - } - - protected override void ActiveChanged(bool active) - { - bool first = !IsActive.HasValue; - - base.ActiveChanged(active); - - if (!first) - { - Source.OnNext(active ? _pushValue : AvaloniaProperty.UnsetValue); - } - } - - protected override void CompletedReceived() - { - base.CompletedReceived(); - - if (!_completed) - { - Source.OnCompleted(); - _completed = true; - } - } - - protected override void ErrorReceived(Exception error) - { - base.ErrorReceived(error); - - if (!_completed) - { - Source.OnError(error); - _completed = true; - } - } - - private void ActivatorCompleted() - { - _completed = true; - Source.OnCompleted(); - } - - private void ActivatorError(Exception e) - { - _completed = true; - Source.OnError(e); - } - } -} diff --git a/src/Avalonia.Styling/Styling/ActivatedValue.cs b/src/Avalonia.Styling/Styling/ActivatedValue.cs deleted file mode 100644 index 908d89b751..0000000000 --- a/src/Avalonia.Styling/Styling/ActivatedValue.cs +++ /dev/null @@ -1,133 +0,0 @@ -// 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.Reactive; - -namespace Avalonia.Styling -{ - /// - /// An value which is switched on or off according to an activator observable. - /// - /// - /// An has two inputs: an activator observable and an - /// . When the activator produces true, the - /// will produce the current value. When the activator - /// produces false it will produce . - /// - internal class ActivatedValue : LightweightObservableBase, IDescription - { - private static readonly object NotSent = new object(); - private IDisposable _activatorSubscription; - private object _value; - private object _last = NotSent; - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// The activated value. - /// The binding description. - public ActivatedValue( - IObservable activator, - object value, - string description) - { - Contract.Requires(activator != null); - - Activator = activator; - Value = value; - Description = description; - Listener = CreateListener(); - } - - /// - /// Gets the activator observable. - /// - public IObservable Activator { get; } - - /// - /// Gets a description of the binding. - /// - public string Description { get; } - - /// - /// Gets a value indicating whether the activator is active. - /// - public bool? IsActive { get; private set; } - - /// - /// Gets the value that will be produced when is true. - /// - public object Value - { - get => _value; - protected set - { - _value = value; - PublishValue(); - } - } - - protected ActivatorListener Listener { get; } - - protected virtual void ActiveChanged(bool active) - { - IsActive = active; - PublishValue(); - } - - protected virtual void CompletedReceived() => PublishCompleted(); - - protected virtual ActivatorListener CreateListener() => new ActivatorListener(this); - - protected override void Deinitialize() - { - _activatorSubscription.Dispose(); - _activatorSubscription = null; - } - - protected virtual void ErrorReceived(Exception error) => PublishError(error); - - protected override void Initialize() - { - _activatorSubscription = Activator.Subscribe(Listener); - } - - protected override void Subscribed(IObserver observer, bool first) - { - if (IsActive == true && !first) - { - observer.OnNext(Value); - } - } - - private void PublishValue() - { - if (IsActive.HasValue) - { - var v = IsActive.Value ? Value : AvaloniaProperty.UnsetValue; - - if (!Equals(v, _last)) - { - PublishNext(v); - _last = v; - } - } - } - - protected class ActivatorListener : IObserver - { - public ActivatorListener(ActivatedValue parent) - { - Parent = parent; - } - - protected ActivatedValue Parent { get; } - - void IObserver.OnCompleted() => Parent.CompletedReceived(); - void IObserver.OnError(Exception error) => Parent.ErrorReceived(error); - void IObserver.OnNext(bool value) => Parent.ActiveChanged(value); - } - } -} diff --git a/src/Avalonia.Styling/Styling/Activators/AndActivator.cs b/src/Avalonia.Styling/Styling/Activators/AndActivator.cs new file mode 100644 index 0000000000..8ab281e8d0 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/AndActivator.cs @@ -0,0 +1,67 @@ +#nullable enable + +using System.Collections.Generic; + +namespace Avalonia.Styling.Activators +{ + internal class AndActivator : StyleActivatorBase, IStyleActivatorSink + { + private List? _sources; + private ulong _flags; + private ulong _mask; + + public int Count => _sources?.Count ?? 0; + + public void Add(IStyleActivator activator) + { + _sources ??= new List(); + _sources.Add(activator); + } + + void IStyleActivatorSink.OnNext(bool value, int tag) + { + if (value) + { + _flags |= 1ul << tag; + } + else + { + _flags &= ~(1ul << tag); + } + + if (_mask != 0) + { + PublishNext(_flags == _mask); + } + } + + protected override void Initialize() + { + if (_sources is object) + { + var i = 0; + + foreach (var source in _sources) + { + source.Subscribe(this, i++); + } + + _mask = (1ul << Count) - 1; + PublishNext(_flags == _mask); + } + } + + protected override void Deinitialize() + { + if (_sources is object) + { + foreach (var source in _sources) + { + source.Unsubscribe(this); + } + } + + _mask = 0; + } + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs b/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs new file mode 100644 index 0000000000..479100ed8a --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/IStyleActivator.cs @@ -0,0 +1,33 @@ +#nullable enable + +using System; + +namespace Avalonia.Styling.Activators +{ + /// + /// Defines a style activator. + /// + /// + /// A style activator is very similar to an `IObservable{bool}` but is optimized for the + /// particular use-case of activating a style according to a selector. It differs from + /// an observable in two major ways: + /// + /// - Can only have a single subscription + /// - The subscription can have a tag associated with it, allowing a subscriber to index + /// into a list of subscriptions without having to allocate additional objects. + /// + public interface IStyleActivator : IDisposable + { + /// + /// Subscribes to the activator. + /// + /// The listener. + /// An optional tag. + void Subscribe(IStyleActivatorSink sink, int tag = 0); + + /// + /// Unsubscribes from the activator. + /// + void Unsubscribe(IStyleActivatorSink sink); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs b/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs new file mode 100644 index 0000000000..a1a6ef5c28 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/IStyleActivatorSink.cs @@ -0,0 +1,17 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + /// + /// Receives notifications from an . + /// + public interface IStyleActivatorSink + { + /// + /// Called when the subscribed activator value changes. + /// + /// The new value. + /// The subscription tag. + void OnNext(bool value, int tag); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/NotActivator.cs b/src/Avalonia.Styling/Styling/Activators/NotActivator.cs new file mode 100644 index 0000000000..4c152a8f0f --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/NotActivator.cs @@ -0,0 +1,13 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + internal class NotActivator : StyleActivatorBase, IStyleActivatorSink + { + private readonly IStyleActivator _source; + public NotActivator(IStyleActivator source) => _source = source; + void IStyleActivatorSink.OnNext(bool value, int tag) => PublishNext(!value); + protected override void Initialize() => _source.Subscribe(this, 0); + protected override void Deinitialize() => _source.Unsubscribe(this); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/OrActivator.cs b/src/Avalonia.Styling/Styling/Activators/OrActivator.cs new file mode 100644 index 0000000000..0220265e10 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/OrActivator.cs @@ -0,0 +1,67 @@ +#nullable enable + +using System.Collections.Generic; + +namespace Avalonia.Styling.Activators +{ + internal class OrActivator : StyleActivatorBase, IStyleActivatorSink + { + private List? _sources; + private ulong _flags; + private bool _initializing; + + public int Count => _sources?.Count ?? 0; + + public void Add(IStyleActivator activator) + { + _sources ??= new List(); + _sources.Add(activator); + } + + void IStyleActivatorSink.OnNext(bool value, int tag) + { + if (value) + { + _flags |= 1ul << tag; + } + else + { + _flags &= ~(1ul << tag); + } + + if (!_initializing) + { + PublishNext(_flags != 0); + } + } + + protected override void Initialize() + { + if (_sources is object) + { + var i = 0; + + _initializing = true; + + foreach (var source in _sources) + { + source.Subscribe(this, i++); + } + + _initializing = false; + PublishNext(_flags != 0); + } + } + + protected override void Deinitialize() + { + if (_sources is object) + { + foreach (var source in _sources) + { + source.Unsubscribe(this); + } + } + } + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs b/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs new file mode 100644 index 0000000000..abf3c1717e --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/PropertyEqualsActivator.cs @@ -0,0 +1,35 @@ +using System; + +#nullable enable + +namespace Avalonia.Styling.Activators +{ + internal class PropertyEqualsActivator : StyleActivatorBase, IObserver + { + private readonly IStyleable _control; + private readonly AvaloniaProperty _property; + private readonly object? _value; + private IDisposable? _subscription; + + public PropertyEqualsActivator( + IStyleable control, + AvaloniaProperty property, + object? value) + { + _control = control ?? throw new ArgumentNullException(nameof(control)); + _property = property ?? throw new ArgumentNullException(nameof(property)); + _value = value; + } + + protected override void Initialize() + { + _subscription = _control.GetObservable(_property).Subscribe(this); + } + + protected override void Deinitialize() => _subscription?.Dispose(); + + void IObserver.OnCompleted() { } + void IObserver.OnError(Exception error) { } + void IObserver.OnNext(object value) => PublishNext(Equals(value, _value)); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs b/src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs new file mode 100644 index 0000000000..725547ed05 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/StyleActivatorBase.cs @@ -0,0 +1,55 @@ +#nullable enable + +namespace Avalonia.Styling.Activators +{ + internal abstract class StyleActivatorBase : IStyleActivator + { + private IStyleActivatorSink? _sink; + private int _tag; + private bool? _value; + + public void Subscribe(IStyleActivatorSink sink, int tag = 0) + { + if (_sink is null) + { + _sink = sink; + _tag = tag; + _value = null; + Initialize(); + } + else + { + throw new AvaloniaInternalException("Cannot subscribe to a StyleActivator more than once."); + } + } + + public void Unsubscribe(IStyleActivatorSink sink) + { + if (_sink != sink) + { + throw new AvaloniaInternalException("StyleActivatorSink is not subscribed."); + } + + _sink = null; + Deinitialize(); + } + + public void PublishNext(bool value) + { + if (_value != value) + { + _value = value; + _sink?.OnNext(value, _tag); + } + } + + public void Dispose() + { + _sink = null; + Deinitialize(); + } + + protected abstract void Initialize(); + protected abstract void Deinitialize(); + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs b/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs new file mode 100644 index 0000000000..906a8303cb --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Collections; + +#nullable enable + +namespace Avalonia.Styling.Activators +{ + internal sealed class StyleClassActivator : StyleActivatorBase + { + private readonly IList _match; + private readonly IAvaloniaReadOnlyList _classes; + + public StyleClassActivator(IAvaloniaReadOnlyList classes, IList match) + { + _classes = classes; + _match = match; + } + + public static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) + { + int remainingMatches = toMatch.Count; + int classesCount = classes.Count; + + // Early bail out - we can't match if control does not have enough classes. + if (classesCount < remainingMatches) + { + return false; + } + + for (var i = 0; i < classesCount; i++) + { + var c = classes[i]; + + if (toMatch.Contains(c)) + { + --remainingMatches; + + // Already matched so we can skip checking other classes. + if (remainingMatches == 0) + { + break; + } + } + } + + return remainingMatches == 0; + } + + + protected override void Initialize() + { + PublishNext(IsMatching()); + _classes.CollectionChanged += ClassesChanged; + } + + protected override void Deinitialize() + { + _classes.CollectionChanged -= ClassesChanged; + } + + private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action != NotifyCollectionChangedAction.Move) + { + PublishNext(IsMatching()); + } + } + + private bool IsMatching() => AreClassesMatching(_classes, _match); + } +} diff --git a/src/Avalonia.Styling/Styling/DescendentSelector.cs b/src/Avalonia.Styling/Styling/DescendentSelector.cs index a81908f23d..08b25f4057 100644 --- a/src/Avalonia.Styling/Styling/DescendentSelector.cs +++ b/src/Avalonia.Styling/Styling/DescendentSelector.cs @@ -2,24 +2,21 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; using Avalonia.LogicalTree; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { internal class DescendantSelector : Selector { private readonly Selector _parent; - private string _selectorString; + private string? _selectorString; - public DescendantSelector(Selector parent) + public DescendantSelector(Selector? parent) { - if (parent == null) - { - throw new InvalidOperationException("Descendant selector must be preceeded by a selector."); - } - - _parent = parent; + _parent = parent ?? throw new InvalidOperationException("Descendant selector must be preceeded by a selector."); } /// @@ -29,7 +26,7 @@ namespace Avalonia.Styling public override bool InTemplate => _parent.InTemplate; /// - public override Type TargetType => null; + public override Type? TargetType => null; public override string ToString() { @@ -43,8 +40,9 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) { - ILogical c = (ILogical)control; - List> descendantMatches = new List>(); + var c = (ILogical)control; + IStyleActivator? descendentMatch = null; + OrActivator? descendantMatches = null; while (c != null) { @@ -56,7 +54,21 @@ namespace Avalonia.Styling if (match.Result == SelectorMatchResult.Sometimes) { - descendantMatches.Add(match.Activator); + if (descendentMatch is null && descendantMatches is null) + { + descendentMatch = match.Activator; + } + else + { + if (descendantMatches is null) + { + descendantMatches = new OrActivator(); + descendantMatches.Add(descendentMatch!); + descendentMatch = null; + } + + descendantMatches.Add(match.Activator!); + } } else if (match.IsMatch) { @@ -65,9 +77,13 @@ namespace Avalonia.Styling } } - if (descendantMatches.Count > 0) + if (descendantMatches is object) + { + return new SelectorMatch(descendantMatches); + } + else if (descendentMatch is object) { - return new SelectorMatch(StyleActivator.Or(descendantMatches)); + return new SelectorMatch(descendentMatch); } else { @@ -75,6 +91,6 @@ namespace Avalonia.Styling } } - protected override Selector MovePrevious() => null; + protected override Selector? MovePrevious() => null; } } diff --git a/src/Avalonia.Styling/Styling/ISetter.cs b/src/Avalonia.Styling/Styling/ISetter.cs index da97638f07..44e43caf85 100644 --- a/src/Avalonia.Styling/Styling/ISetter.cs +++ b/src/Avalonia.Styling/Styling/ISetter.cs @@ -3,6 +3,8 @@ using System; +#nullable enable + namespace Avalonia.Styling { /// @@ -11,11 +13,16 @@ namespace Avalonia.Styling public interface ISetter { /// - /// Applies the setter to a control. + /// Instances a setter on a control. /// - /// The style that is being applied. - /// The control. - /// An optional activator. - IDisposable Apply(IStyle style, IStyleable control, IObservable activator); + /// The control. + /// Whether the parent style has an activator. + /// An . + /// + /// This method should return an which can be used to apply + /// the setter to the specified control. Note that it should not apply the setter value + /// until is called. + /// + ISetterInstance Instance(IStyleable target, bool hasActivator); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Styling/Styling/ISetterInstance.cs b/src/Avalonia.Styling/Styling/ISetterInstance.cs new file mode 100644 index 0000000000..ebfc227d12 --- /dev/null +++ b/src/Avalonia.Styling/Styling/ISetterInstance.cs @@ -0,0 +1,20 @@ +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// Represents a setter that has been instanced on a control. + /// + public interface ISetterInstance + { + /// + /// Activates the setter. + /// + public void Activate(); + + /// + /// Deactivates the setter. + /// + public void Deactivate(); + } +} diff --git a/src/Avalonia.Styling/Styling/IStyle.cs b/src/Avalonia.Styling/Styling/IStyle.cs index da2a08f04d..8151aacf54 100644 --- a/src/Avalonia.Styling/Styling/IStyle.cs +++ b/src/Avalonia.Styling/Styling/IStyle.cs @@ -3,6 +3,8 @@ using Avalonia.Controls; +#nullable enable + namespace Avalonia.Styling { /// @@ -13,17 +15,11 @@ namespace Avalonia.Styling /// /// Attaches the style to a control if the style's selector matches. /// - /// The control to attach to. - /// - /// The control that contains this style. May be null. - /// + /// The control to attach to. + /// The element that hosts the style. /// - /// True if the style can match a control of type - /// (even if it does not match this control specifically); false if the style - /// can never match. + /// A describing how the style matches the control. /// - bool Attach(IStyleable control, IStyleHost container); - - void Detach(); + SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host); } } diff --git a/src/Avalonia.Styling/Styling/IStyleInstance.cs b/src/Avalonia.Styling/Styling/IStyleInstance.cs new file mode 100644 index 0000000000..cb094badd2 --- /dev/null +++ b/src/Avalonia.Styling/Styling/IStyleInstance.cs @@ -0,0 +1,22 @@ +using System; + +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// Represents a style that has been instanced on a control. + /// + public interface IStyleInstance : IDisposable + { + /// + /// Gets the source style. + /// + IStyle Source { get; } + + /// + /// Instructs the style to start acting upon the control. + /// + void Start(); + } +} diff --git a/src/Avalonia.Styling/Styling/IStyleable.cs b/src/Avalonia.Styling/Styling/IStyleable.cs index 5ad97d8a61..b01c779bcc 100644 --- a/src/Avalonia.Styling/Styling/IStyleable.cs +++ b/src/Avalonia.Styling/Styling/IStyleable.cs @@ -4,6 +4,8 @@ using System; using Avalonia.Collections; +#nullable enable + namespace Avalonia.Styling { /// @@ -11,11 +13,6 @@ namespace Avalonia.Styling /// public interface IStyleable : IAvaloniaObject, INamed { - /// - /// Signaled when the control's style should be removed. - /// - IObservable StyleDetach { get; } - /// /// Gets the list of classes for the control. /// @@ -29,6 +26,17 @@ namespace Avalonia.Styling /// /// Gets the template parent of this element if the control comes from a template. /// - ITemplatedControl TemplatedParent { get; } + ITemplatedControl? TemplatedParent { get; } + + /// + /// Notifies the element that a style has been applied. + /// + /// The style instance. + void StyleApplied(IStyleInstance instance); + + /// + /// Detaches all styles applied to the element. + /// + void DetachStyles(); } } diff --git a/src/Avalonia.Styling/Styling/NotSelector.cs b/src/Avalonia.Styling/Styling/NotSelector.cs index bcf76620be..6428535a12 100644 --- a/src/Avalonia.Styling/Styling/NotSelector.cs +++ b/src/Avalonia.Styling/Styling/NotSelector.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Linq; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -11,16 +13,16 @@ namespace Avalonia.Styling /// internal class NotSelector : Selector { - private readonly Selector _previous; + private readonly Selector? _previous; private readonly Selector _argument; - private string _selectorString; + private string? _selectorString; /// /// Initializes a new instance of the class. /// /// The previous selector. /// The selector to be not-ed. - public NotSelector(Selector previous, Selector argument) + public NotSelector(Selector? previous, Selector argument) { _previous = previous; _argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument."); @@ -33,7 +35,7 @@ namespace Avalonia.Styling public override bool IsCombinator => false; /// - public override Type TargetType => _previous?.TargetType; + public override Type? TargetType => _previous?.TargetType; /// public override string ToString() @@ -61,12 +63,12 @@ namespace Avalonia.Styling case SelectorMatchResult.NeverThisType: return SelectorMatch.AlwaysThisType; case SelectorMatchResult.Sometimes: - return new SelectorMatch(innerResult.Activator.Select(x => !x)); + return new SelectorMatch(new NotActivator(innerResult.Activator!)); default: throw new InvalidOperationException("Invalid SelectorMatchResult."); } } - protected override Selector MovePrevious() => _previous; + protected override Selector? MovePrevious() => _previous; } } diff --git a/src/Avalonia.Styling/Styling/OrSelector.cs b/src/Avalonia.Styling/Styling/OrSelector.cs index 58c5c778fb..9c76a38f45 100644 --- a/src/Avalonia.Styling/Styling/OrSelector.cs +++ b/src/Avalonia.Styling/Styling/OrSelector.cs @@ -3,6 +3,9 @@ using System; using System.Collections.Generic; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -12,8 +15,8 @@ namespace Avalonia.Styling internal class OrSelector : Selector { private readonly IReadOnlyList _selectors; - private string _selectorString; - private Type _targetType; + private string? _selectorString; + private Type? _targetType; /// /// Initializes a new instance of the class. @@ -21,8 +24,15 @@ namespace Avalonia.Styling /// The selectors to OR. public OrSelector(IReadOnlyList selectors) { - Contract.Requires(selectors != null); - Contract.Requires(selectors.Count > 1); + if (selectors is null) + { + throw new ArgumentNullException(nameof(selectors)); + } + + if (selectors.Count <= 1) + { + throw new ArgumentException("Need more than one selector to OR."); + } _selectors = selectors; } @@ -34,7 +44,7 @@ namespace Avalonia.Styling public override bool IsCombinator => false; /// - public override Type TargetType + public override Type? TargetType { get { @@ -60,7 +70,8 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) { - var activators = new List>(); + IStyleActivator? activator = null; + OrActivator? activators = null; var neverThisInstance = false; foreach (var selector in _selectors) @@ -76,18 +87,32 @@ namespace Avalonia.Styling neverThisInstance = true; break; case SelectorMatchResult.Sometimes: - activators.Add(match.Activator); + if (activator is null && activators is null) + { + activator = match.Activator; + } + else + { + if (activators is null) + { + activators = new OrActivator(); + activators.Add(activator!); + activator = null; + } + + activators.Add(match.Activator!); + } break; } } - if (activators.Count > 1) + if (activators is object) { - return new SelectorMatch(StyleActivator.Or(activators)); + return new SelectorMatch(activators); } - else if (activators.Count == 1) + else if (activator is object) { - return new SelectorMatch(activators[0]); + return new SelectorMatch(activator); } else if (neverThisInstance) { @@ -99,11 +124,11 @@ namespace Avalonia.Styling } } - protected override Selector MovePrevious() => null; + protected override Selector? MovePrevious() => null; - private Type EvaluateTargetType() + private Type? EvaluateTargetType() { - var result = default(Type); + Type? result = null; foreach (var selector in _selectors) { diff --git a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs index cfc0998fe0..d7e1f46a94 100644 --- a/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Styling/Styling/PropertyEqualsSelector.cs @@ -2,8 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Linq; using System.Text; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -13,14 +15,14 @@ namespace Avalonia.Styling /// internal class PropertyEqualsSelector : Selector { - private readonly Selector _previous; + private readonly Selector? _previous; private readonly AvaloniaProperty _property; - private readonly object _value; - private string _selectorString; + private readonly object? _value; + private string? _selectorString; - public PropertyEqualsSelector(Selector previous, AvaloniaProperty property, object value) + public PropertyEqualsSelector(Selector? previous, AvaloniaProperty property, object? value) { - Contract.Requires(property != null); + property = property ?? throw new ArgumentNullException(nameof(property)); _previous = previous; _property = property; @@ -33,13 +35,8 @@ namespace Avalonia.Styling /// public override bool IsCombinator => false; - /// - /// Gets the name of the control to match. - /// - public string Name { get; private set; } - /// - public override Type TargetType => _previous?.TargetType; + public override Type? TargetType => _previous?.TargetType; /// public override string ToString() @@ -77,7 +74,7 @@ namespace Avalonia.Styling { if (subscribe) { - return new SelectorMatch(control.GetObservable(_property).Select(v => Equals(v ?? string.Empty, _value))); + return new SelectorMatch(new PropertyEqualsActivator(control, _property, _value)); } else { @@ -86,6 +83,6 @@ namespace Avalonia.Styling } } - protected override Selector MovePrevious() => _previous; + protected override Selector? MovePrevious() => _previous; } } diff --git a/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs new file mode 100644 index 0000000000..74d7f98398 --- /dev/null +++ b/src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs @@ -0,0 +1,48 @@ +using System; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Styling +{ + internal class PropertySetterBindingInstance : ISetterInstance + { + private readonly IStyleable _target; + private readonly AvaloniaProperty _property; + private readonly BindingPriority _priority; + private readonly InstancedBinding _binding; + private IDisposable? _subscription; + private bool _isActive; + + public PropertySetterBindingInstance( + IStyleable target, + AvaloniaProperty property, + BindingPriority priority, + IBinding binding) + { + _target = target; + _property = property; + _priority = priority; + _binding = binding.Initiate(target, property).WithPriority(priority); + } + + public void Activate() + { + if (!_isActive) + { + _subscription = BindingOperations.Apply(_target, _property, _binding, null); + _isActive = true; + } + } + + public void Deactivate() + { + if (_isActive) + { + _subscription?.Dispose(); + _subscription = null; + _isActive = false; + } + } + } +} diff --git a/src/Avalonia.Styling/Styling/PropertySetterInstance.cs b/src/Avalonia.Styling/Styling/PropertySetterInstance.cs new file mode 100644 index 0000000000..284ca8cdd0 --- /dev/null +++ b/src/Avalonia.Styling/Styling/PropertySetterInstance.cs @@ -0,0 +1,82 @@ +using System; +using Avalonia.Data; + +#nullable enable + +namespace Avalonia.Styling +{ + internal class PropertySetterInstance : ISetterInstance + { + private readonly IStyleable _target; + private readonly StyledPropertyBase? _styledProperty; + private readonly DirectPropertyBase? _directProperty; + private readonly BindingPriority _priority; + private readonly T _value; + private IDisposable? _subscription; + private bool _isActive; + + public PropertySetterInstance( + IStyleable target, + StyledPropertyBase property, + BindingPriority priority, + T value) + { + _target = target; + _styledProperty = property; + _priority = priority; + _value = value; + } + + public PropertySetterInstance( + IStyleable target, + DirectPropertyBase property, + BindingPriority priority, + T value) + { + _target = target; + _directProperty = property; + _priority = priority; + _value = value; + } + + public void Activate() + { + if (!_isActive) + { + if (_styledProperty is object) + { + _subscription = _target.SetValue(_styledProperty, _value, _priority); + } + else + { + _target.SetValue(_directProperty!, _value); + } + + _isActive = true; + } + } + + public void Deactivate() + { + if (_isActive) + { + if (_subscription is null) + { + if (_styledProperty is object) + { + _target.ClearValue(_styledProperty); + } + else + { + _target.ClearValue(_directProperty!); + } + } + else + { + _subscription.Dispose(); + _subscription = null; + } + } + } + } +} diff --git a/src/Avalonia.Styling/Styling/Selector.cs b/src/Avalonia.Styling/Styling/Selector.cs index 7d4e92baeb..6d74eb8842 100644 --- a/src/Avalonia.Styling/Styling/Selector.cs +++ b/src/Avalonia.Styling/Styling/Selector.cs @@ -2,9 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Diagnostics; -using Avalonia.Utilities; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -30,7 +30,7 @@ namespace Avalonia.Styling /// /// Gets the target type of the selector, if available. /// - public abstract Type TargetType { get; } + public abstract Type? TargetType { get; } /// /// Tries to match the selector with a control. @@ -43,8 +43,8 @@ namespace Avalonia.Styling /// A . public SelectorMatch Match(IStyleable control, bool subscribe = true) { - ValueSingleOrList> inputs = default; - + IStyleActivator? activator = null; + AndActivator? activators = null; var selector = this; var alwaysThisType = true; var hitCombinator = false; @@ -69,21 +69,39 @@ namespace Avalonia.Styling } else if (match.Result == SelectorMatchResult.Sometimes) { - Debug.Assert(match.Activator != null); + if (match.Activator is null) + { + throw new AvaloniaInternalException( + "SelectorMatch returned Sometimes but there is no activator."); + } + + if (activator is null && activators is null) + { + activator = match.Activator; + } + else + { + if (activators is null) + { + activators = new AndActivator(); + activators.Add(activator!); + activator = null; + } - inputs.Add(match.Activator); + activators.Add(match.Activator); + } } selector = selector.MovePrevious(); } - if (inputs.HasList) + if (activators is object) { - return new SelectorMatch(StyleActivator.And(inputs.List)); + return new SelectorMatch(activators); } - else if (inputs.IsSingle) + else if (activator is object) { - return new SelectorMatch(inputs.Single); + return new SelectorMatch(activator); } else { @@ -107,6 +125,6 @@ namespace Avalonia.Styling /// /// Moves to the previous selector. /// - protected abstract Selector MovePrevious(); + protected abstract Selector? MovePrevious(); } } diff --git a/src/Avalonia.Styling/Styling/SelectorMatch.cs b/src/Avalonia.Styling/Styling/SelectorMatch.cs index 63b89e9e97..3cc84a0b57 100644 --- a/src/Avalonia.Styling/Styling/SelectorMatch.cs +++ b/src/Avalonia.Styling/Styling/SelectorMatch.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -21,9 +24,9 @@ namespace Avalonia.Styling NeverThisInstance, /// - /// The selector always matches this type. + /// The selector matches this instance based on the . /// - AlwaysThisType, + Sometimes, /// /// The selector always matches this instance, but doesn't always match this type. @@ -31,9 +34,9 @@ namespace Avalonia.Styling AlwaysThisInstance, /// - /// The selector matches this instance based on the . + /// The selector always matches this type. /// - Sometimes, + AlwaysThisType, } /// @@ -43,7 +46,7 @@ namespace Avalonia.Styling /// A selector match describes whether and how a matches a control, and /// in addition whether the selector can ever match a control of the same type. /// - public class SelectorMatch + public readonly struct SelectorMatch { /// /// A selector match with the result of . @@ -70,20 +73,24 @@ namespace Avalonia.Styling /// result. /// /// The match activator. - public SelectorMatch(IObservable match) + public SelectorMatch(IStyleActivator match) { - Contract.Requires(match != null); + match = match ?? throw new ArgumentNullException(nameof(match)); Result = SelectorMatchResult.Sometimes; Activator = match; } - private SelectorMatch(SelectorMatchResult result) => Result = result; + private SelectorMatch(SelectorMatchResult result) + { + Result = result; + Activator = null; + } /// /// Gets a value indicating whether the match was positive. /// - public bool IsMatch => Result >= SelectorMatchResult.AlwaysThisType; + public bool IsMatch => Result >= SelectorMatchResult.Sometimes; /// /// Gets the result of the match. @@ -91,9 +98,9 @@ namespace Avalonia.Styling public SelectorMatchResult Result { get; } /// - /// Gets an observable which tracks the selector match, in the case of selectors that can + /// Gets an activator which tracks the selector match, in the case of selectors that can /// change over time. /// - public IObservable Activator { get; } + public IStyleActivator? Activator { get; } } } diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index b880ecb01c..08e8a699b8 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -2,11 +2,12 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Disposables; using Avalonia.Animation; using Avalonia.Data; using Avalonia.Metadata; -using Avalonia.Reactive; +using Avalonia.Utilities; + +#nullable enable namespace Avalonia.Styling { @@ -17,9 +18,9 @@ namespace Avalonia.Styling /// A is used to set a value on a /// depending on a condition. /// - public class Setter : ISetter, IAnimationSetter + public class Setter : ISetter, IAnimationSetter, IAvaloniaPropertyVisitor { - private object _value; + private object? _value; /// /// Initializes a new instance of the class. @@ -42,11 +43,7 @@ namespace Avalonia.Styling /// /// Gets or sets the property to set. /// - public AvaloniaProperty Property - { - get; - set; - } + public AvaloniaProperty? Property { get; set; } /// /// Gets or sets the property value. @@ -54,13 +51,9 @@ namespace Avalonia.Styling [Content] [AssignBinding] [DependsOn(nameof(Property))] - public object Value + public object? Value { - get - { - return _value; - } - + get => _value; set { (value as ISetterValue)?.Initialize(this); @@ -68,99 +61,71 @@ namespace Avalonia.Styling } } - /// - /// Applies the setter to a control. - /// - /// The style that is being applied. - /// The control. - /// An optional activator. - public IDisposable Apply(IStyle style, IStyleable control, IObservable activator) + public ISetterInstance Instance(IStyleable target, bool hasActivator) { - Contract.Requires(control != null); + target = target ?? throw new ArgumentNullException(nameof(target)); - if (Property == null) + if (Property is null) { throw new InvalidOperationException("Setter.Property must be set."); } - var value = Value; - var binding = value as IBinding; + var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style; - if (binding == null) + if (Value is IBinding binding) { - if (value is ITemplate template) - { - bool isPropertyOfTypeITemplate = typeof(ITemplate).IsAssignableFrom(Property.PropertyType); - - if (!isPropertyOfTypeITemplate) - { - var materialized = template.Build(); - value = materialized; - } - } - - if (activator == null) - { - return control.Bind(Property, ObservableEx.SingleValue(value), BindingPriority.Style); - } - else - { - var description = style?.ToString(); - - var activated = new ActivatedValue(activator, value, description); - return control.Bind(Property, activated, BindingPriority.StyleTrigger); - } + return new PropertySetterBindingInstance(target, Property, priority, binding); } else { - var source = binding.Initiate(control, Property); + var value = Value; - if (source != null) + if (value is ITemplate template && + !typeof(ITemplate).IsAssignableFrom(Property.PropertyType)) { - var cloned = Clone(source, source.Mode == BindingMode.Default ? Property.GetMetadata(control.GetType()).DefaultBindingMode : source.Mode, style, activator); - return BindingOperations.Apply(control, Property, cloned, null); + value = template.Build(); } - } - return Disposable.Empty; + var data = new SetterVisitorData + { + target = target, + priority = priority, + value = value, + }; + + Property.Accept(this, ref data); + return data.result!; + } } - private InstancedBinding Clone(InstancedBinding sourceInstance, BindingMode mode, IStyle style, IObservable activator) + void IAvaloniaPropertyVisitor.Visit( + StyledPropertyBase property, + ref SetterVisitorData data) { - if (activator != null) - { - var description = style?.ToString(); + data.result = new PropertySetterInstance( + data.target, + property, + data.priority, + (T)data.value); + } - switch (mode) - { - case BindingMode.OneTime: - if (sourceInstance.Observable != null) - { - var activated = new ActivatedObservable(activator, sourceInstance.Observable, description); - return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger); - } - else - { - var activated = new ActivatedValue(activator, sourceInstance.Value, description); - return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger); - } - case BindingMode.OneWay: - { - var activated = new ActivatedObservable(activator, sourceInstance.Observable, description); - return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger); - } - default: - { - var activated = new ActivatedSubject(activator, sourceInstance.Subject, description); - return new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger); - } - } + void IAvaloniaPropertyVisitor.Visit( + DirectPropertyBase property, + ref SetterVisitorData data) + { + data.result = new PropertySetterInstance( + data.target, + property, + data.priority, + (T)data.value); + } - } - else - { - return sourceInstance.WithPriority(BindingPriority.Style); - } + private struct SetterVisitorData + { + public IStyleable target; + public BindingPriority priority; + public object? value; + public ISetterInstance? result; } } } diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 22db7adfe4..c607ee60e5 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -3,12 +3,12 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; -using System.Reactive.Linq; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Metadata; +#nullable enable + namespace Avalonia.Styling { /// @@ -16,15 +16,10 @@ namespace Avalonia.Styling /// public class Style : AvaloniaObject, IStyle, ISetResourceParent { - private static Dictionary _applied = - new Dictionary(); - private IResourceNode _parent; - - private CompositeDisposable _subscriptions; - - private IResourceDictionary _resources; - - private IList _animations; + private IResourceNode? _parent; + private IResourceDictionary? _resources; + private List? _setters; + private List? _animations; /// /// Initializes a new instance of the class. @@ -37,13 +32,13 @@ namespace Avalonia.Styling /// Initializes a new instance of the class. /// /// The style selector. - public Style(Func selector) + public Style(Func selector) { Selector = selector(null); } /// - public event EventHandler ResourcesChanged; + public event EventHandler? ResourcesChanged; /// /// Gets or sets a dictionary of style resources. @@ -53,7 +48,7 @@ namespace Avalonia.Styling get => _resources ?? (Resources = new ResourceDictionary()); set { - Contract.Requires(value != null); + value = value ?? throw new ArgumentNullException(nameof(value)); var hadResources = false; @@ -76,117 +71,45 @@ namespace Avalonia.Styling /// /// Gets or sets the style's selector. /// - public Selector Selector { get; set; } + public Selector? Selector { get; set; } /// - /// Gets or sets the style's setters. + /// Gets the style's setters. /// [Content] - public IList Setters { get; set; } = new List(); + public IList Setters => _setters ??= new List(); - public IList Animations - { - get - { - return _animations ?? (_animations = new List()); - } - } - - private CompositeDisposable Subscriptions - { - get - { - return _subscriptions ?? (_subscriptions = new CompositeDisposable(2)); - } - } + /// + /// Gets the style's animations. + /// + public IList Animations => _animations ??= new List(); /// - IResourceNode IResourceNode.ResourceParent => _parent; + IResourceNode? IResourceNode.ResourceParent => _parent; /// bool IResourceProvider.HasResources => _resources?.Count > 0; /// - public bool Attach(IStyleable control, IStyleHost container) + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) { - if (Selector != null) - { - var match = Selector.Match(control); - - if (match.IsMatch) - { - var controlSubscriptions = GetSubscriptions(control); - - var animatable = control as Animatable; - - var setters = Setters; - var settersCount = setters.Count; - var animations = Animations; - var animationsCount = animations.Count; - - var subs = new CompositeDisposable(settersCount + (animatable != null ? animationsCount : 0) + 1); - - if (animatable != null) - { - for (var i = 0; i < animationsCount; i++) - { - var animation = animations[i]; - var obsMatch = match.Activator; - - if (match.Result == SelectorMatchResult.AlwaysThisType || - match.Result == SelectorMatchResult.AlwaysThisInstance) - { - obsMatch = Observable.Return(true); - } - - var sub = animation.Apply(animatable, null, obsMatch); - subs.Add(sub); - } - } - - for (var i = 0; i < settersCount; i++) - { - var setter = setters[i]; - var sub = setter.Apply(this, control, match.Activator); - subs.Add(sub); - } + target = target ?? throw new ArgumentNullException(nameof(target)); - subs.Add(Disposable.Create((subs, Subscriptions) , state => state.Subscriptions.Remove(state.subs))); + var match = Selector is object ? Selector.Match(target) : + target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; - controlSubscriptions.Add(subs); - Subscriptions.Add(subs); - } - - return match.Result != SelectorMatchResult.NeverThisType; - } - else if (control == container) + if (match.IsMatch && _setters is object) { - var setters = Setters; - var settersCount = setters.Count; - - var controlSubscriptions = GetSubscriptions(control); - - var subs = new CompositeDisposable(settersCount + 1); - - for (var i = 0; i < settersCount; i++) - { - var setter = setters[i]; - var sub = setter.Apply(this, control, null); - subs.Add(sub); - } - - subs.Add(Disposable.Create((subs, Subscriptions), state => state.Subscriptions.Remove(state.subs))); - - controlSubscriptions.Add(subs); - Subscriptions.Add(subs); - return true; + var instance = new StyleInstance(this, target, _setters, match.Activator); + target.StyleApplied(instance); + instance.Start(); } - return false; + return match.Result; } /// - public bool TryGetResource(object key, out object result) + public bool TryGetResource(object key, out object? result) { result = null; return _resources?.TryGetResource(key, out result) ?? false; @@ -224,44 +147,12 @@ namespace Avalonia.Styling if (parent == null) { - Detach(); + //Detach(); } _parent = parent; } - public void Detach() - { - _subscriptions?.Dispose(); - _subscriptions = null; - } - - private static CompositeDisposable GetSubscriptions(IStyleable control) - { - if (!_applied.TryGetValue(control, out var subscriptions)) - { - subscriptions = new CompositeDisposable(3); - subscriptions.Add(control.StyleDetach.Subscribe(ControlDetach)); - _applied.Add(control, subscriptions); - } - - return subscriptions; - } - - /// - /// Called when a control's is signaled to remove - /// all applied styles. - /// - /// The control. - private static void ControlDetach(IStyleable control) - { - var subscriptions = _applied[control]; - - subscriptions.Dispose(); - - _applied.Remove(control); - } - private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e) { ResourcesChanged?.Invoke(this, e); diff --git a/src/Avalonia.Styling/Styling/StyleActivator.cs b/src/Avalonia.Styling/Styling/StyleActivator.cs deleted file mode 100644 index 63945037d8..0000000000 --- a/src/Avalonia.Styling/Styling/StyleActivator.cs +++ /dev/null @@ -1,56 +0,0 @@ -// 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 System.Collections.Generic; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; - -namespace Avalonia.Styling -{ - public enum ActivatorMode - { - And, - Or, - } - - public static class StyleActivator - { - public static IObservable And(IList> inputs) - { - if (inputs.Count == 0) - { - throw new ArgumentException("StyleActivator.And inputs may not be empty."); - } - else if (inputs.Count == 1) - { - return inputs[0]; - } - else - { - return inputs.CombineLatest() - .Select(values => values.All(x => x)) - .DistinctUntilChanged(); - } - } - - public static IObservable Or(IList> inputs) - { - if (inputs.Count == 0) - { - throw new ArgumentException("StyleActivator.Or inputs may not be empty."); - } - else if (inputs.Count == 1) - { - return inputs[0]; - } - else - { - return inputs.CombineLatest() - .Select(values => values.Any(x => x)) - .DistinctUntilChanged(); - } - } - } -} diff --git a/src/Avalonia.Styling/Styling/StyleInstance.cs b/src/Avalonia.Styling/Styling/StyleInstance.cs new file mode 100644 index 0000000000..6977f19f59 --- /dev/null +++ b/src/Avalonia.Styling/Styling/StyleInstance.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using Avalonia.Styling.Activators; + +#nullable enable + +namespace Avalonia.Styling +{ + internal class StyleInstance : IStyleInstance, IStyleActivatorSink + { + private readonly List _setters; + private readonly IStyleActivator? _activator; + private bool _active; + + public StyleInstance( + IStyle source, + IStyleable target, + IReadOnlyList setters, + IStyleActivator? activator = null) + { + setters = setters ?? throw new ArgumentNullException(nameof(setters)); + + Source = source ?? throw new ArgumentNullException(nameof(source)); + Target = target ?? throw new ArgumentNullException(nameof(target)); + + _setters = new List(setters.Count); + _activator = activator; + + foreach (var setter in setters) + { + _setters.Add(setter.Instance(target, activator is object)); + } + } + + public IStyle Source { get; } + public IStyleable Target { get; } + + public void Start() + { + if (_activator == null) + { + ActivatorChanged(true); + } + else + { + _activator.Subscribe(this, 0); + } + } + + public void Dispose() + { + ActivatorChanged(false); + _activator?.Dispose(); + } + + private void ActivatorChanged(bool value) + { + if (_active != value) + { + _active = value; + + if (_active) + { + foreach (var setter in _setters) + { + setter.Activate(); + } + } + else + { + foreach (var setter in _setters) + { + setter.Deactivate(); + } + } + } + } + + void IStyleActivatorSink.OnNext(bool value, int tag) => ActivatorChanged(value); + } +} diff --git a/src/Avalonia.Styling/Styling/Styler.cs b/src/Avalonia.Styling/Styling/Styler.cs index 7ac5c89005..cfd9f65aee 100644 --- a/src/Avalonia.Styling/Styling/Styler.cs +++ b/src/Avalonia.Styling/Styling/Styler.cs @@ -3,35 +3,34 @@ using System; +#nullable enable + namespace Avalonia.Styling { public class Styler : IStyler { - public void ApplyStyles(IStyleable control) + public void ApplyStyles(IStyleable target) { - var styleHost = control as IStyleHost; + target = target ?? throw new ArgumentNullException(nameof(target)); - if (styleHost != null) + if (target is IStyleHost styleHost) { - ApplyStyles(control, styleHost); + ApplyStyles(target, styleHost); } } - private void ApplyStyles(IStyleable control, IStyleHost styleHost) + private void ApplyStyles(IStyleable target, IStyleHost host) { - Contract.Requires(control != null); - Contract.Requires(styleHost != null); - - var parentContainer = styleHost.StylingParent; + var parent = host.StylingParent; - if (parentContainer != null) + if (parent != null) { - ApplyStyles(control, parentContainer); + ApplyStyles(target, parent); } - if (styleHost.IsStylesInitialized) + if (host.IsStylesInitialized) { - styleHost.Styles.Attach(control, styleHost); + host.Styles.TryAttach(target, host); } } } diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index fd38c39650..fc579266e8 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -9,6 +9,8 @@ using System.Linq; using Avalonia.Collections; using Avalonia.Controls; +#nullable enable + namespace Avalonia.Styling { /// @@ -16,10 +18,10 @@ namespace Avalonia.Styling /// public class Styles : AvaloniaObject, IAvaloniaList, IStyle, ISetResourceParent { - private IResourceNode _parent; - private IResourceDictionary _resources; - private AvaloniaList _styles = new AvaloniaList(); - private Dictionary> _cache; + private readonly AvaloniaList _styles = new AvaloniaList(); + private IResourceNode? _parent; + private IResourceDictionary? _resources; + private Dictionary?>? _cache; public Styles() { @@ -60,6 +62,12 @@ namespace Avalonia.Styling () => { }); } + public Styles(IResourceNode parent) + : this() + { + _parent = parent; + } + public event NotifyCollectionChangedEventHandler CollectionChanged { add => _styles.CollectionChanged += value; @@ -67,7 +75,7 @@ namespace Avalonia.Styling } /// - public event EventHandler ResourcesChanged; + public event EventHandler? ResourcesChanged; /// public int Count => _styles.Count; @@ -83,7 +91,7 @@ namespace Avalonia.Styling get => _resources ?? (Resources = new ResourceDictionary()); set { - Contract.Requires(value != null); + value = value ?? throw new ArgumentNullException(nameof(Resources)); var hadResources = false; @@ -104,7 +112,7 @@ namespace Avalonia.Styling } /// - IResourceNode IResourceNode.ResourceParent => _parent; + IResourceNode? IResourceNode.ResourceParent => _parent; /// bool ICollection.IsReadOnly => false; @@ -119,66 +127,50 @@ namespace Avalonia.Styling set => _styles[index] = value; } - /// - /// Attaches the style to a control if the style's selector matches. - /// - /// The control to attach to. - /// - /// The control that contains this style. May be null. - /// - public bool Attach(IStyleable control, IStyleHost container) + /// + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) { - if (_cache == null) - { - _cache = new Dictionary>(); - } + _cache ??= new Dictionary?>(); - if (_cache.TryGetValue(control.StyleKey, out var cached)) + if (_cache.TryGetValue(target.StyleKey, out var cached)) { - if (cached != null) + if (cached is object) { foreach (var style in cached) { - style.Attach(control, container); + style.TryAttach(target, host); } - return true; + return SelectorMatchResult.AlwaysThisType; + } + else + { + return SelectorMatchResult.NeverThisType; } - - return false; } else { - List result = null; + List? matches = null; - foreach (var style in this) + foreach (var child in this) { - if (style.Attach(control, container)) + if (child.TryAttach(target, host) != SelectorMatchResult.NeverThisType) { - if (result == null) - { - result = new List(); - } - - result.Add(style); + matches ??= new List(); + matches.Add(child); } } - _cache.Add(control.StyleKey, result); - return result != null; - } - } - - public void Detach() - { - foreach (IStyle style in this) - { - style.Detach(); + _cache.Add(target.StyleKey, matches); + + return matches is null ? + SelectorMatchResult.NeverThisType : + SelectorMatchResult.AlwaysThisType; } } /// - public bool TryGetResource(object key, out object value) + public bool TryGetResource(object key, out object? value) { if (_resources != null && _resources.TryGetResource(key, out value)) { diff --git a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs index 401fa54fb5..71b8828cba 100644 --- a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs @@ -3,11 +3,10 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Reflection; using System.Text; -using Avalonia.Collections; -using Avalonia.Reactive; +using Avalonia.Styling.Activators; + +#nullable enable namespace Avalonia.Styling { @@ -17,13 +16,12 @@ namespace Avalonia.Styling /// internal class TypeNameAndClassSelector : Selector { - private readonly Selector _previous; + private readonly Selector? _previous; private readonly Lazy> _classes = new Lazy>(() => new List()); - private Type _targetType; - - private string _selectorString; + private Type? _targetType; + private string? _selectorString; - public static TypeNameAndClassSelector OfType(Selector previous, Type targetType) + public static TypeNameAndClassSelector OfType(Selector? previous, Type targetType) { var result = new TypeNameAndClassSelector(previous); result._targetType = targetType; @@ -32,7 +30,7 @@ namespace Avalonia.Styling return result; } - public static TypeNameAndClassSelector Is(Selector previous, Type targetType) + public static TypeNameAndClassSelector Is(Selector? previous, Type targetType) { var result = new TypeNameAndClassSelector(previous); result._targetType = targetType; @@ -41,7 +39,7 @@ namespace Avalonia.Styling return result; } - public static TypeNameAndClassSelector ForName(Selector previous, string name) + public static TypeNameAndClassSelector ForName(Selector? previous, string name) { var result = new TypeNameAndClassSelector(previous); result.Name = name; @@ -49,7 +47,7 @@ namespace Avalonia.Styling return result; } - public static TypeNameAndClassSelector ForClass(Selector previous, string className) + public static TypeNameAndClassSelector ForClass(Selector? previous, string className) { var result = new TypeNameAndClassSelector(previous); result.Classes.Add(className); @@ -57,7 +55,7 @@ namespace Avalonia.Styling return result; } - protected TypeNameAndClassSelector(Selector previous) + protected TypeNameAndClassSelector(Selector? previous) { _previous = previous; } @@ -68,10 +66,10 @@ namespace Avalonia.Styling /// /// Gets the name of the control to match. /// - public string Name { get; set; } + public string? Name { get; set; } /// - public override Type TargetType => _targetType ?? _previous?.TargetType; + public override Type? TargetType => _targetType ?? _previous?.TargetType; /// public override bool IsCombinator => false; @@ -130,12 +128,12 @@ namespace Avalonia.Styling { if (subscribe) { - var observable = new ClassObserver(control.Classes, _classes.Value); + var observable = new StyleClassActivator(control.Classes, _classes.Value); return new SelectorMatch(observable); } - if (!AreClassesMatching(control.Classes, Classes)) + if (!StyleClassActivator.AreClassesMatching(control.Classes, Classes)) { return SelectorMatch.NeverThisInstance; } @@ -144,7 +142,7 @@ namespace Avalonia.Styling return Name == null ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance; } - protected override Selector MovePrevious() => _previous; + protected override Selector? MovePrevious() => _previous; private string BuildSelectorString() { @@ -190,80 +188,5 @@ namespace Avalonia.Styling return builder.ToString(); } - - private static bool AreClassesMatching(IReadOnlyList classes, IList toMatch) - { - int remainingMatches = toMatch.Count; - int classesCount = classes.Count; - - // Early bail out - we can't match if control does not have enough classes. - if (classesCount < remainingMatches) - { - return false; - } - - for (var i = 0; i < classesCount; i++) - { - var c = classes[i]; - - if (toMatch.Contains(c)) - { - --remainingMatches; - - // Already matched so we can skip checking other classes. - if (remainingMatches == 0) - { - break; - } - } - } - - return remainingMatches == 0; - } - - private sealed class ClassObserver : LightweightObservableBase - { - private readonly IList _match; - private readonly IAvaloniaReadOnlyList _classes; - private bool _hasMatch; - - public ClassObserver(IAvaloniaReadOnlyList classes, IList match) - { - _classes = classes; - _match = match; - } - - protected override void Deinitialize() => _classes.CollectionChanged -= ClassesChanged; - - protected override void Initialize() - { - _hasMatch = IsMatching(); - _classes.CollectionChanged += ClassesChanged; - } - - protected override void Subscribed(IObserver observer, bool first) - { - observer.OnNext(_hasMatch); - } - - private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (e.Action != NotifyCollectionChangedAction.Move) - { - var hasMatch = IsMatching(); - - if (hasMatch != _hasMatch) - { - PublishNext(hasMatch); - _hasMatch = hasMatch; - } - } - } - - private bool IsMatching() - { - return AreClassesMatching(_classes, _match); - } - } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 41eab79ed8..e5e28b344f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -4,6 +4,7 @@ using Avalonia.Styling; using System; using Avalonia.Controls; +using System.Collections.Generic; namespace Avalonia.Markup.Xaml.Styling { @@ -67,23 +68,7 @@ namespace Avalonia.Markup.Xaml.Styling IResourceNode IResourceNode.ResourceParent => _parent; /// - public bool Attach(IStyleable control, IStyleHost container) - { - if (Source != null) - { - return Loaded.Attach(control, container); - } - - return false; - } - - public void Detach() - { - if (Source != null) - { - Loaded.Detach(); - } - } + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost host) => Loaded.TryAttach(target, host); /// public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value); diff --git a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs index d24a646f74..b9d0b53728 100644 --- a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs +++ b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs @@ -45,7 +45,7 @@ namespace Avalonia.Benchmarks.Styling { _window.Styles.Add(new Style(x => x.OfType().Class("foo").Class("bar").Class("baz")) { - Setters = new[] + Setters = { new Setter(TextBox.TextProperty, "foo"), } diff --git a/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs b/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs index 7bccd65c81..7dad517e51 100644 --- a/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.CompilerServices; using Avalonia.Controls; +using Avalonia.Styling; using Avalonia.UnitTests; using BenchmarkDotNet.Attributes; @@ -34,9 +35,8 @@ namespace Avalonia.Benchmarks.Styling { var styles = UnitTestApplication.Current.Styles; - styles.Attach(_control, UnitTestApplication.Current); - - styles.Detach(); + styles.TryAttach(_control, UnitTestApplication.Current); + ((IStyleable)_control).DetachStyles(); } public void Dispose() diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs index d36d0b609b..53b5b87ea2 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -363,7 +363,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter( TemplatedControl.TemplateProperty, @@ -399,7 +399,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter( TemplatedControl.TemplateProperty, @@ -438,7 +438,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter( TemplatedControl.TemplateProperty, @@ -458,7 +458,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter( TemplatedControl.TemplateProperty, diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index a9e86d71ee..6101e7b3d4 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -163,7 +163,7 @@ namespace Avalonia.Controls.UnitTests { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter(TemplatedControl.TemplateProperty, template) } diff --git a/tests/Avalonia.Controls.UnitTests/UserControlTests.cs b/tests/Avalonia.Controls.UnitTests/UserControlTests.cs index 9d3e568582..fddef4ec88 100644 --- a/tests/Avalonia.Controls.UnitTests/UserControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/UserControlTests.cs @@ -25,7 +25,7 @@ namespace Avalonia.Controls.UnitTests { new Style(x => x.OfType()) { - Setters = new[] + Setters = { new Setter(TemplatedControl.TemplateProperty, GetTemplate()) } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs index d82300b964..ec9a6ba77f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/AvaloniaPropertyConverterTest.cs @@ -139,7 +139,15 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters get { throw new NotImplementedException(); } } - IObservable IStyleable.StyleDetach { get; } + public void DetachStyles() + { + throw new NotImplementedException(); + } + + public void StyleApplied(IStyleInstance instance) + { + throw new NotImplementedException(); + } } private class AttachedOwner diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs index 2dc6c4a7fb..44a40af93d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/StyleTests.cs @@ -53,42 +53,7 @@ namespace Avalonia.Markup.Xaml.UnitTests } }; - setter.Apply(null, control, null); - Assert.Equal("foo", control.Text); - - control.Text = "bar"; - Assert.Equal("bar", data.Foo); - } - } - - [Fact] - public void Setter_With_TwoWay_Binding_And_Activator_Should_Update_Source() - { - using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) - { - var data = new Data - { - Foo = "foo", - }; - - var control = new TextBox - { - DataContext = data, - }; - - var setter = new Setter - { - Property = TextBox.TextProperty, - Value = new Binding - { - Path = "Foo", - Mode = BindingMode.TwoWay - } - }; - - var activator = Observable.Never().StartWith(true); - - setter.Apply(null, control, activator); + setter.Instance(control, false).Activate(); Assert.Equal("foo", control.Text); control.Text = "bar"; diff --git a/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs b/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs deleted file mode 100644 index 7773d4767a..0000000000 --- a/tests/Avalonia.Styling.UnitTests/ActivatedObservableTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -// 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 System.Collections.Generic; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Xunit; - -namespace Avalonia.Styling.UnitTests -{ - public class ActivatedObservableTests - { - [Fact] - public void Should_Produce_Correct_Values() - { - var activator = new BehaviorSubject(false); - var source = new BehaviorSubject(1); - var target = new ActivatedObservable(activator, source, string.Empty); - var result = new List(); - - target.Subscribe(x => result.Add(x)); - - activator.OnNext(true); - source.OnNext(2); - activator.OnNext(false); - source.OnNext(3); - activator.OnNext(true); - - Assert.Equal( - new[] - { - AvaloniaProperty.UnsetValue, - 1, - 2, - AvaloniaProperty.UnsetValue, - 3, - }, - result); - } - - [Fact] - public void Should_Complete_When_Source_Completes() - { - var activator = new BehaviorSubject(false); - var source = new BehaviorSubject(1); - var target = new ActivatedObservable(activator, source, string.Empty); - var completed = false; - - target.Subscribe(_ => { }, () => completed = true); - source.OnCompleted(); - - Assert.True(completed); - } - - [Fact] - public void Should_Error_When_Source_Errors() - { - var activator = new BehaviorSubject(false); - var source = new BehaviorSubject(1); - var target = new ActivatedObservable(activator, source, string.Empty); - var error = new Exception(); - var completed = false; - - target.Subscribe(_ => { }, x => completed = true); - source.OnError(error); - - Assert.True(completed); - } - } -} diff --git a/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs b/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs deleted file mode 100644 index 03f91d97a1..0000000000 --- a/tests/Avalonia.Styling.UnitTests/ActivatedSubjectTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -// 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 System.Reactive.Disposables; -using System.Reactive.Subjects; -using Xunit; - -namespace Avalonia.Styling.UnitTests -{ - public class ActivatedSubjectTests - { - [Fact] - public void Should_Set_Values() - { - var activator = new BehaviorSubject(false); - var source = new TestSubject(); - var target = new ActivatedSubject(activator, source, string.Empty); - - target.Subscribe(); - target.OnNext("bar"); - Assert.Equal(AvaloniaProperty.UnsetValue, source.Value); - activator.OnNext(true); - target.OnNext("baz"); - Assert.Equal("baz", source.Value); - activator.OnNext(false); - Assert.Equal(AvaloniaProperty.UnsetValue, source.Value); - target.OnNext("bax"); - activator.OnNext(true); - Assert.Equal("bax", source.Value); - } - - [Fact] - public void Should_Invoke_OnCompleted_On_Activator_Completed() - { - var activator = new BehaviorSubject(false); - var source = new TestSubject(); - var target = new ActivatedSubject(activator, source, string.Empty); - - target.Subscribe(); - activator.OnCompleted(); - - Assert.True(source.Completed); - } - - [Fact] - public void Should_Invoke_OnError_On_Activator_Error() - { - var activator = new BehaviorSubject(false); - var source = new TestSubject(); - var target = new ActivatedSubject(activator, source, string.Empty); - var targetError = default(Exception); - var error = new Exception(); - - target.Subscribe(_ => { }, e => targetError = e); - activator.OnError(error); - - Assert.Same(error, source.Error); - Assert.Same(error, targetError); - } - - private class TestSubject : ISubject - { - private IObserver _observer; - - public bool Completed { get; set; } - public Exception Error { get; set; } - public object Value { get; set; } = AvaloniaProperty.UnsetValue; - - public void OnCompleted() - { - Completed = true; - } - - public void OnError(Exception error) - { - Error = error; - } - - public void OnNext(object value) - { - Value = value; - } - - public IDisposable Subscribe(IObserver observer) - { - _observer = observer; - return Disposable.Empty; - } - } - } -} diff --git a/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs b/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs deleted file mode 100644 index 92a7c1bd1f..0000000000 --- a/tests/Avalonia.Styling.UnitTests/ActivatedValueTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -// 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 System.Collections.Generic; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Microsoft.Reactive.Testing; -using Xunit; - -namespace Avalonia.Styling.UnitTests -{ - public class ActivatedValueTests - { - [Fact] - public void Should_Produce_Correct_Values() - { - var activator = new BehaviorSubject(false); - var target = new ActivatedValue(activator, 1, string.Empty); - var result = new List(); - - target.Subscribe(x => result.Add(x)); - - activator.OnNext(true); - activator.OnNext(false); - - Assert.Equal(new[] { AvaloniaProperty.UnsetValue, 1, AvaloniaProperty.UnsetValue }, result); - } - - [Fact] - public void Should_Complete_When_Activator_Completes() - { - var activator = new BehaviorSubject(false); - var target = new ActivatedValue(activator, 1, string.Empty); - var completed = false; - - target.Subscribe(_ => { }, () => completed = true); - activator.OnCompleted(); - - Assert.True(completed); - } - - [Fact] - public void Should_Error_When_Activator_Errors() - { - var activator = new BehaviorSubject(false); - var target = new ActivatedValue(activator, 1, string.Empty); - var error = new Exception(); - var completed = false; - - target.Subscribe(_ => { }, x => completed = true); - activator.OnError(error); - - Assert.True(completed); - } - - [Fact] - public void Should_Unsubscribe_From_Activator_When_All_Subscriptions_Disposed() - { - var scheduler = new TestScheduler(); - var activator1 = scheduler.CreateColdObservable(); - var activator2 = scheduler.CreateColdObservable(); - var activator = StyleActivator.And(new[] { activator1, activator2 }); - var target = new ActivatedValue(activator, 1, string.Empty); - - var subscription = target.Subscribe(_ => { }); - Assert.Equal(1, activator1.Subscriptions.Count); - Assert.Equal(Subscription.Infinite, activator1.Subscriptions[0].Unsubscribe); - - subscription.Dispose(); - Assert.Equal(1, activator1.Subscriptions.Count); - Assert.Equal(0, activator1.Subscriptions[0].Unsubscribe); - } - } -} diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs index fd25b17ba4..00b90f1239 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs @@ -36,7 +36,7 @@ namespace Avalonia.Styling.UnitTests { var control = new Control1 { - Classes = new Classes { "foo" }, + Classes = { "foo" }, }; var target = default(Selector).Class("foo"); @@ -51,7 +51,7 @@ namespace Avalonia.Styling.UnitTests { var control = new Control1 { - Classes = new Classes { "bar" }, + Classes = { "bar" }, }; var target = default(Selector).Class("foo"); @@ -66,7 +66,7 @@ namespace Avalonia.Styling.UnitTests { var control = new Control1 { - Classes = new Classes { "foo" }, + Classes = { "foo" }, TemplatedParent = new Mock().Object, }; @@ -83,7 +83,7 @@ namespace Avalonia.Styling.UnitTests var control = new Control1(); var target = default(Selector).Class("foo"); - var activator = target.Match(control).Activator; + var activator = target.Match(control).Activator.ToObservable(); Assert.False(await activator.Take(1)); control.Classes.Add("foo"); @@ -95,11 +95,11 @@ namespace Avalonia.Styling.UnitTests { var control = new Control1 { - Classes = new Classes { "foo" }, + Classes = { "foo" }, }; var target = default(Selector).Class("foo"); - var activator = target.Match(control).Activator; + var activator = target.Match(control).Activator.ToObservable(); Assert.True(await activator.Take(1)); control.Classes.Remove("foo"); @@ -111,7 +111,7 @@ namespace Avalonia.Styling.UnitTests { var control = new Control1(); var target = default(Selector).Class("foo").Class("bar"); - var activator = target.Match(control).Activator; + var activator = target.Match(control).Activator.ToObservable(); Assert.False(await activator.Take(1)); control.Classes.Add("foo"); @@ -128,7 +128,7 @@ namespace Avalonia.Styling.UnitTests // Test for #1698 var control = new Control1 { - Classes = new Classes { "foo" }, + Classes = { "foo" }, }; var target = default(Selector).Class("foo"); diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs index 099562b1cf..1128120824 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs @@ -90,7 +90,7 @@ namespace Avalonia.Styling.UnitTests child.LogicalParent = parent; var selector = default(Selector).OfType().Class("foo").Descendant().OfType(); - var activator = selector.Match(child).Activator; + var activator = selector.Match(child).Activator.ToObservable(); Assert.False(await activator.Take(1)); parent.Classes.Add("foo"); diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs index e8be44ed3b..a1ced14108 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Multiple.cs @@ -85,6 +85,35 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(SelectorMatchResult.NeverThisType, match.Result); } + [Fact] + public void Control_With_Class_Descendent_Of_Control_With_Two_Classes() + { + var textBlock = new TextBlock(); + var control = new Button { Content = textBlock }; + + control.ApplyTemplate(); + + var selector = default(Selector) + .OfType