From ac119f0d74e2cdc386922a84ba933f6fd74e1fc8 Mon Sep 17 00:00:00 2001 From: jcguo Date: Fri, 28 Aug 2026 11:19:19 +0800 Subject: [PATCH 1/8] Upgrade Avalonia and align ReactiveUI dependencies --- build/_build.csproj | 2 +- build/_build.csproj.DotSettings | 8 +++- .../AvalonStudio.Shell.Extensibility.csproj | 9 ++-- .../Behaviors/AttachNativeMenuBehavior.cs | 2 +- .../HideWhenNativeMenuExportedBehavior.cs | 2 +- .../Commands/CommandIconService.cs | 2 +- .../Controls/DocumentTabControl.cs | 45 +------------------ .../Controls/DocumentTabControl.xaml | 4 +- .../Controls/DocumentTabItem.cs | 8 ++-- .../Controls/EditableTextBlock.cs | 16 +++---- .../Controls/StyledText.cs | 19 ++++++++ .../Controls/ViewLocatorDataTemplate.cs | 2 +- .../Controls/ViewModelViewHost.cs | 10 ++--- .../Converters/BitmapValueConverter.cs | 5 +-- .../Converters/NativeMenuConverter.cs | 4 +- .../Dialogs/ModalDialogViewModelBase.cs | 2 +- .../MVVM/ViewLocator.cs | 4 +- .../MainMenu/Views/MainMenuView.paml | 13 ++---- .../MainMenu/Views/MainMenuView.paml.cs | 4 +- .../Shell/IShell.cs | 4 +- .../Utils/IEnumerableUtils.cs | 4 +- .../AvalonStudio.Shell.csproj | 10 ++--- .../AvalonStudioPerspective.cs | 2 +- .../Controls/MetroWindow.cs | 35 ++++++--------- .../Controls/ModalDialog.paml.cs | 4 +- .../Controls/StatusBar.paml.cs | 4 +- src/AvalonStudio.Shell/Controls/ToolBar.paml | 6 +-- .../Controls/ToolBar.paml.cs | 4 +- src/AvalonStudio.Shell/DockExtensions.cs | 34 +++----------- .../Docking/AvalonStudioDocumentDock.cs | 11 ++--- .../Docking/AvalonStudioToolDock.cs | 2 + .../Docking/DefaultLayoutFactory.cs | 14 +++--- .../Docking/Views/HomeView.cs | 5 ++- .../Docking/Views/MainView.cs | 5 ++- src/AvalonStudio.Shell/MenuShellView.xaml.cs | 2 +- src/AvalonStudio.Shell/Shell.cs | 5 +-- src/AvalonStudio.Shell/ShellView.paml | 17 +------ src/AvalonStudio.Shell/ShellView.paml.cs | 2 +- src/AvalonStudio.Shell/ShellViewModel.cs | 10 ++--- .../AvalonStudio.Utils.csproj | 3 +- .../Behaviors/CommandBasedBehavior.cs | 30 +++++++------ .../Behaviors/OpenCloseWindowBehavior.cs | 2 +- src/Packages.targets | 6 ++- src/ShellExampleApp/AboutDialogViewModel.cs | 2 +- src/ShellExampleApp/App.paml | 9 ++-- src/ShellExampleApp/MainWindow.xaml | 14 ------ src/ShellExampleApp/ShellExampleApp.csproj | 5 ++- src/Versions.props | 18 +++++--- src/nuget.config | 4 -- 49 files changed, 172 insertions(+), 262 deletions(-) diff --git a/build/_build.csproj b/build/_build.csproj index 4988ad3..9d4629c 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + net8.0 false False diff --git a/build/_build.csproj.DotSettings b/build/_build.csproj.DotSettings index 96e392e..28494fb 100644 --- a/build/_build.csproj.DotSettings +++ b/build/_build.csproj.DotSettings @@ -1,4 +1,4 @@ - + False Implicit Implicit @@ -13,11 +13,15 @@ False <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy> True True True True + True True True True - True + True + True diff --git a/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj b/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj index 8746b64..3fa7dd9 100644 --- a/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj +++ b/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net8.0 @@ -11,12 +11,11 @@ - - - + - + + diff --git a/src/AvalonStudio.Shell.Extensibility/Behaviors/AttachNativeMenuBehavior.cs b/src/AvalonStudio.Shell.Extensibility/Behaviors/AttachNativeMenuBehavior.cs index ea90886..23de599 100644 --- a/src/AvalonStudio.Shell.Extensibility/Behaviors/AttachNativeMenuBehavior.cs +++ b/src/AvalonStudio.Shell.Extensibility/Behaviors/AttachNativeMenuBehavior.cs @@ -33,7 +33,7 @@ protected override void OnAttached() { if (Menu != null) { - if (AssociatedObject.GetVisualRoot() is TopLevel tl) + if (TopLevel.GetTopLevel(AssociatedObject) is TopLevel tl) { NativeMenu.SetMenu(tl, Menu); } diff --git a/src/AvalonStudio.Shell.Extensibility/Behaviors/HideWhenNativeMenuExportedBehavior.cs b/src/AvalonStudio.Shell.Extensibility/Behaviors/HideWhenNativeMenuExportedBehavior.cs index 40a766a..6132ee4 100644 --- a/src/AvalonStudio.Shell.Extensibility/Behaviors/HideWhenNativeMenuExportedBehavior.cs +++ b/src/AvalonStudio.Shell.Extensibility/Behaviors/HideWhenNativeMenuExportedBehavior.cs @@ -17,7 +17,7 @@ protected override void OnAttached() .Take(1) .Subscribe(x => { - if (AssociatedObject.GetVisualRoot() is TopLevel tl) + if (TopLevel.GetTopLevel(AssociatedObject) is TopLevel tl) { if (NativeMenu.GetIsNativeMenuExported(tl)) { diff --git a/src/AvalonStudio.Shell.Extensibility/Commands/CommandIconService.cs b/src/AvalonStudio.Shell.Extensibility/Commands/CommandIconService.cs index 08a9637..dd149e1 100644 --- a/src/AvalonStudio.Shell.Extensibility/Commands/CommandIconService.cs +++ b/src/AvalonStudio.Shell.Extensibility/Commands/CommandIconService.cs @@ -16,7 +16,7 @@ public DrawingGroup GetCompletionKindImage(string icon) { if (!_cache.TryGetValue(icon, out var image)) { - if (Application.Current.Styles.TryGetResource(icon.ToString(), out object resource)) + if (Application.Current.Styles.TryGetResource(icon.ToString(), null, out object resource)) { image = resource as DrawingGroup; _cache.Add(icon, image); diff --git a/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.cs b/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.cs index c3a43ac..6225034 100644 --- a/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.cs +++ b/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.cs @@ -2,11 +2,6 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; -using System.Collections.Specialized; -using System.Linq; -using Avalonia.LogicalTree; -using AvalonStudio.Utils; -using Avalonia.Controls.Generators; namespace AvalonStudio.Controls { @@ -33,43 +28,5 @@ public IDataTemplate HeaderTemplate set { SetValue(HeaderTemplateProperty, value); } } - protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - base.ItemsCollectionChanged(sender, e); - } - - protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e) - { - base.ItemsChanged(e); - - if (Items.Count() > 0) - { - SelectedIndex = 0; - } - } - - /// - /// Selects the content of a tab item. - /// - /// The tab item. - /// The content. - private static object SelectContent(object o) - { - var content = o as IContentControl; - - if (content != null) - { - return content.Content; - } - else - { - return o; - } - } - - protected override IItemContainerGenerator CreateItemContainerGenerator() - { - return null; - } } -} \ No newline at end of file +} diff --git a/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.xaml b/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.xaml index 1f0292d..f10eca1 100644 --- a/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.xaml +++ b/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.xaml @@ -7,7 +7,7 @@ - + - \ No newline at end of file + diff --git a/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml.cs b/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml.cs index 52e05c4..3ba2e99 100644 --- a/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml.cs +++ b/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml.cs @@ -3,7 +3,7 @@ namespace AvalonStudio.Shell.Extensibility.MainMenu.Views { - public class MainMenuView : UserControl + public partial class MainMenuView : UserControl { public MainMenuView() { @@ -15,4 +15,4 @@ private void InitializeComponent() AvaloniaXamlLoader.Load(this); } } -} \ No newline at end of file +} diff --git a/src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs b/src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs index 2f7c1b3..4b6f60f 100644 --- a/src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs +++ b/src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs @@ -2,7 +2,7 @@ using AvalonStudio.Documents; using AvalonStudio.Extensibility.Dialogs; using AvalonStudio.MVVM; -using Dock.Model; +using Dock.Model.Core; using Dock.Model.Controls; using System.Collections.Generic; @@ -45,6 +45,6 @@ public interface IShell IReadOnlyList Documents { get; } - IPanel Overlay { get; } + Panel Overlay { get; } } } diff --git a/src/AvalonStudio.Shell.Extensibility/Utils/IEnumerableUtils.cs b/src/AvalonStudio.Shell.Extensibility/Utils/IEnumerableUtils.cs index c94682d..7609be6 100644 --- a/src/AvalonStudio.Shell.Extensibility/Utils/IEnumerableUtils.cs +++ b/src/AvalonStudio.Shell.Extensibility/Utils/IEnumerableUtils.cs @@ -50,7 +50,7 @@ public static int Count(this IEnumerable items, Func predicate) public static int IndexOf(this IEnumerable items, object item) { - Contract.Requires(items != null); + if (items == null) throw new ArgumentNullException(nameof(items)); var list = items as IList; @@ -78,7 +78,7 @@ public static int IndexOf(this IEnumerable items, object item) public static object ElementAt(this IEnumerable items, int index) { - Contract.Requires(items != null); + if (items == null) throw new ArgumentNullException(nameof(items)); var list = items as IList; diff --git a/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj b/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj index 647b2b4..8905c33 100644 --- a/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj +++ b/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net8.0 @@ -12,7 +12,6 @@ - @@ -20,12 +19,11 @@ - - - + - + + diff --git a/src/AvalonStudio.Shell/AvalonStudioPerspective.cs b/src/AvalonStudio.Shell/AvalonStudioPerspective.cs index 1b73134..d66805a 100644 --- a/src/AvalonStudio.Shell/AvalonStudioPerspective.cs +++ b/src/AvalonStudio.Shell/AvalonStudioPerspective.cs @@ -1,6 +1,6 @@ using AvalonStudio.Extensibility; using AvalonStudio.MVVM; -using Dock.Model; +using Dock.Model.Core; using Dock.Model.Controls; using ReactiveUI; using System; diff --git a/src/AvalonStudio.Shell/Controls/MetroWindow.cs b/src/AvalonStudio.Shell/Controls/MetroWindow.cs index 454e5b7..91bb619 100644 --- a/src/AvalonStudio.Shell/Controls/MetroWindow.cs +++ b/src/AvalonStudio.Shell/Controls/MetroWindow.cs @@ -5,7 +5,6 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Media; -using Avalonia.Styling; using AvalonStudio.Extensibility.Theme; using AvalonStudio.Extensibility.Utils; using System; @@ -13,7 +12,7 @@ namespace AvalonStudio.Shell.Controls { - public class MetroWindow : Window, IStyleable + public class MetroWindow : Window { public enum ClassLongIndex : int { @@ -65,16 +64,21 @@ public MetroWindow() if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { // do this in code or we get a delay in osx. - HasSystemDecorations = false; ClientDecorations = true; if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - var classes = (int)GetClassLongPtr(this.PlatformImpl.Handle.Handle, (int)ClassLongIndex.GCL_STYLE); + // Avalonia 11+ exposes the native handle through TopLevel rather + // than PlatformImpl.Handle. + var nativeHandle = TryGetPlatformHandle()?.Handle ?? IntPtr.Zero; + if (nativeHandle != IntPtr.Zero) + { + var classes = (int)GetClassLongPtr(nativeHandle, (int)ClassLongIndex.GCL_STYLE); - classes |= (int)0x00020000; + classes |= (int)0x00020000; - SetClassLong(this.PlatformImpl.Handle.Handle, ClassLongIndex.GCL_STYLE, new IntPtr(classes)); + SetClassLong(nativeHandle, ClassLongIndex.GCL_STYLE, new IntPtr(classes)); + } } } else @@ -84,8 +88,6 @@ public MetroWindow() if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - HasSystemDecorations = true; - // This will need implementing properly once this is supported by avalonia itself. //var color = (ColorTheme.CurrentTheme.Background as SolidColorBrush).Color; //(PlatformImpl as Avalonia.Native.WindowImpl).SetTitleBarColor(color); @@ -106,8 +108,6 @@ public MetroWindow() private Grid _leftVerticalGrip; private Button _minimiseButton; - private bool _mouseDown; - private Point _mouseDownPosition; private Button _restoreButton; private Path _restoreButtonPanelPath; private Grid _rightVerticalGrip; @@ -129,7 +129,7 @@ public Control TitleBarContent set { SetValue(TitleBarContentProperty, value); } } - Type IStyleable.StyleKey => typeof(MetroWindow); + protected override Type StyleKeyOverride => typeof(MetroWindow); protected override void OnPointerPressed(PointerPressedEventArgs e) { @@ -167,26 +167,17 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) } else if (_titleBar.IsPointerOver) { - _mouseDown = true; - _mouseDownPosition = e.GetPosition(this); - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { BeginMoveDrag(e); - _mouseDown = false; } } - else - { - _mouseDown = false; - } base.OnPointerPressed(e); } protected override void OnPointerReleased(PointerReleasedEventArgs e) { - _mouseDown = false; base.OnPointerReleased(e); } @@ -204,13 +195,13 @@ private void ToggleWindowState() } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if(change.Property == WindowStateProperty) { - PseudoClasses.Set(":maximised", change.NewValue.HasValue && change.NewValue.GetValueOrDefault() == WindowState.Maximized); + PseudoClasses.Set(":maximised", change.NewValue is WindowState state && state == WindowState.Maximized); } } diff --git a/src/AvalonStudio.Shell/Controls/ModalDialog.paml.cs b/src/AvalonStudio.Shell/Controls/ModalDialog.paml.cs index 7b495c9..48793f7 100644 --- a/src/AvalonStudio.Shell/Controls/ModalDialog.paml.cs +++ b/src/AvalonStudio.Shell/Controls/ModalDialog.paml.cs @@ -3,7 +3,7 @@ namespace AvalonStudio.Shell.Controls { - public class ModalDialog : MetroWindow + public partial class ModalDialog : MetroWindow { public ModalDialog() { @@ -15,4 +15,4 @@ private void InitializeComponent() AvaloniaXamlLoader.Load(this); } } -} \ No newline at end of file +} diff --git a/src/AvalonStudio.Shell/Controls/StatusBar.paml.cs b/src/AvalonStudio.Shell/Controls/StatusBar.paml.cs index a6ca3c4..da0df1e 100644 --- a/src/AvalonStudio.Shell/Controls/StatusBar.paml.cs +++ b/src/AvalonStudio.Shell/Controls/StatusBar.paml.cs @@ -3,7 +3,7 @@ namespace AvalonStudio.Shell.Controls { - public class StatusBar : UserControl + public partial class StatusBar : UserControl { public StatusBar() { @@ -15,4 +15,4 @@ private void InitializeComponent() AvaloniaXamlLoader.Load(this); } } -} \ No newline at end of file +} diff --git a/src/AvalonStudio.Shell/Controls/ToolBar.paml b/src/AvalonStudio.Shell/Controls/ToolBar.paml index 2846bb5..36a28b6 100644 --- a/src/AvalonStudio.Shell/Controls/ToolBar.paml +++ b/src/AvalonStudio.Shell/Controls/ToolBar.paml @@ -26,7 +26,7 @@ - + @@ -40,7 +40,7 @@ @@ -48,4 +48,4 @@ - \ No newline at end of file + diff --git a/src/AvalonStudio.Shell/Controls/ToolBar.paml.cs b/src/AvalonStudio.Shell/Controls/ToolBar.paml.cs index d505088..101ed18 100644 --- a/src/AvalonStudio.Shell/Controls/ToolBar.paml.cs +++ b/src/AvalonStudio.Shell/Controls/ToolBar.paml.cs @@ -4,7 +4,7 @@ namespace AvalonStudio.Shell.Controls { - public class ToolBar : UserControl + public partial class ToolBar : UserControl { public ToolBar() { @@ -16,4 +16,4 @@ private void InitializeComponent() AvaloniaXamlLoader.Load(this); } } -} \ No newline at end of file +} diff --git a/src/AvalonStudio.Shell/DockExtensions.cs b/src/AvalonStudio.Shell/DockExtensions.cs index b78f3ba..8b04723 100644 --- a/src/AvalonStudio.Shell/DockExtensions.cs +++ b/src/AvalonStudio.Shell/DockExtensions.cs @@ -1,14 +1,15 @@ using AvalonStudio.Documents; using AvalonStudio.MVVM; -using Dock.Model; +using Dock.Model.Core; using Dock.Model.Controls; +using Dock.Model.ReactiveUI.Core; using ReactiveUI; using System; using System.Linq; namespace AvalonStudio.Shell { - abstract class AvalonStudioTab : ViewModel, IDockable where T : IDockableViewModel + abstract class AvalonStudioTab : DockableBase where T : IDockableViewModel { T _model; @@ -22,30 +23,7 @@ public AvalonStudioTab(T model) .Subscribe(title => Title = title); } - public string Id { get; set; } - - - private string _title; - - public string Title - { - get { return _title; } - set { this.RaiseAndSetIfChanged(ref _title, value); } - } - - - private object _context; - - public object Context - { - get { return _context; } - set { this.RaiseAndSetIfChanged(ref _context, value); } - } - - public IDockable Owner { get; set; } - public IFactory Factory { get; set; } - - public bool OnClose() + public override bool OnClose() { return _model.OnClose(); } @@ -55,7 +33,7 @@ public void OnOpen() _model.OnOpen(); } - public void OnSelected() + public override void OnSelected() { } @@ -105,7 +83,7 @@ public static IDockable Dock(this IDock dock, IDockableViewModel model, bool add if (currentTab != null) { - dock.Factory.UpdateDockable(currentTab, currentTab.Owner); + dock.Factory.InitDockable(currentTab, currentTab.Owner); } } diff --git a/src/AvalonStudio.Shell/Docking/AvalonStudioDocumentDock.cs b/src/AvalonStudio.Shell/Docking/AvalonStudioDocumentDock.cs index 727785f..0c8d71c 100644 --- a/src/AvalonStudio.Shell/Docking/AvalonStudioDocumentDock.cs +++ b/src/AvalonStudio.Shell/Docking/AvalonStudioDocumentDock.cs @@ -1,6 +1,7 @@ using AvalonStudio.Shell; -using Dock.Model; +using Dock.Model.Core; using Dock.Model.Controls; +using Dock.Model.ReactiveUI.Controls; namespace AvalonStudio.Docking { @@ -12,13 +13,9 @@ public override bool OnClose() return base.OnClose(); } - public override IDockable Clone() + public IDockable Clone() { - var result = base.Clone() as AvalonStudioDocumentDock; - - result.VisibleDockables = this.VisibleDockables; - - return result; + return this; } } } diff --git a/src/AvalonStudio.Shell/Docking/AvalonStudioToolDock.cs b/src/AvalonStudio.Shell/Docking/AvalonStudioToolDock.cs index ba57d21..550b507 100644 --- a/src/AvalonStudio.Shell/Docking/AvalonStudioToolDock.cs +++ b/src/AvalonStudio.Shell/Docking/AvalonStudioToolDock.cs @@ -1,5 +1,7 @@ using AvalonStudio.Shell; +using Dock.Model.Core; using Dock.Model.Controls; +using Dock.Model.ReactiveUI.Controls; namespace AvalonStudio.Docking { diff --git a/src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs b/src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs index e80a6a3..329c4e1 100644 --- a/src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs +++ b/src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs @@ -1,7 +1,9 @@ using AvaloniaDemo.ViewModels.Views; using Dock.Avalonia.Controls; -using Dock.Model; +using Dock.Model.Core; using Dock.Model.Controls; +using Dock.Model.ReactiveUI; +using Dock.Model.ReactiveUI.Controls; using ReactiveUI; using System; using System.Collections.Generic; @@ -32,7 +34,7 @@ public override IDocumentDock CreateDocumentDock() } /// - public override IDock CreateLayout() + public override IRootDock CreateLayout() { var documentDock = new AvalonStudioDocumentDock { @@ -108,7 +110,7 @@ private void DefaultLayoutFactory_CollectionChanged(object sender, System.Collec } - public override void UpdateDockable(IDockable view, IDockable parent) + public void UpdateDockable(IDockable view, IDockable parent) { view.Owner = parent; @@ -133,7 +135,7 @@ public override void UpdateDockable(IDockable view, IDockable parent) { foreach (var child in rootDock.Windows) { - UpdateDockWindow(child, view); + InitDockWindow(child, view); } } } @@ -157,13 +159,13 @@ public override void InitLayout(IDockable layout) if (layout is IRootDock layoutWindowsHost) { - layoutWindowsHost.ShowWindows(); + layoutWindowsHost.ShowWindows.Execute(null); if (layout is IDock layoutViewsHost) { layoutViewsHost.ActiveDockable = layoutViewsHost.DefaultDockable; if (layoutViewsHost.ActiveDockable is IRootDock currentViewWindowsHost) { - currentViewWindowsHost.ShowWindows(); + currentViewWindowsHost.ShowWindows.Execute(null); } } } diff --git a/src/AvalonStudio.Shell/Docking/Views/HomeView.cs b/src/AvalonStudio.Shell/Docking/Views/HomeView.cs index 6ba555e..2917621 100644 --- a/src/AvalonStudio.Shell/Docking/Views/HomeView.cs +++ b/src/AvalonStudio.Shell/Docking/Views/HomeView.cs @@ -1,12 +1,13 @@ // Copyright (c) Wiesław Šoltés. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Dock.Model; +using Dock.Model.Core; +using Dock.Model.ReactiveUI.Core; namespace AvaloniaDemo.ViewModels.Views { public class HomeView : DockBase { - public override IDockable Clone() + public IDockable Clone() { throw new System.NotImplementedException(); } diff --git a/src/AvalonStudio.Shell/Docking/Views/MainView.cs b/src/AvalonStudio.Shell/Docking/Views/MainView.cs index b381066..99e43c0 100644 --- a/src/AvalonStudio.Shell/Docking/Views/MainView.cs +++ b/src/AvalonStudio.Shell/Docking/Views/MainView.cs @@ -1,12 +1,13 @@ // Copyright (c) Wiesław Šoltés. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Dock.Model; +using Dock.Model.Core; +using Dock.Model.ReactiveUI.Core; namespace AvaloniaDemo.ViewModels.Views { public class MainView : DockBase { - public override IDockable Clone() + public IDockable Clone() { return this; } diff --git a/src/AvalonStudio.Shell/MenuShellView.xaml.cs b/src/AvalonStudio.Shell/MenuShellView.xaml.cs index df4fe63..f37a32d 100644 --- a/src/AvalonStudio.Shell/MenuShellView.xaml.cs +++ b/src/AvalonStudio.Shell/MenuShellView.xaml.cs @@ -7,7 +7,7 @@ namespace AvalonStudio.Shell { - public class MenuShellView : UserControl + public partial class MenuShellView : UserControl { public MenuShellView() { diff --git a/src/AvalonStudio.Shell/Shell.cs b/src/AvalonStudio.Shell/Shell.cs index ce22f6e..bba5424 100644 --- a/src/AvalonStudio.Shell/Shell.cs +++ b/src/AvalonStudio.Shell/Shell.cs @@ -3,7 +3,7 @@ using Avalonia.ReactiveUI; using AvalonStudio.Extensibility; using AvalonStudio.Shell.Extensibility.Platforms; -using Dock.Model; +using Dock.Model.Core; namespace AvalonStudio.Shell { @@ -11,8 +11,7 @@ public static class Shell { public delegate void ShellAppMainDelegate(string[] args); - public static void StartShellApp(this TAppBuilder builder, string appName, ShellAppMainDelegate main, string[] args, IFactory layoutFactory = null) - where TAppBuilder : AppBuilderBase, new() + public static void StartShellApp(this AppBuilder builder, string appName, ShellAppMainDelegate main, string[] args, IFactory layoutFactory = null) { builder .UseReactiveUI() diff --git a/src/AvalonStudio.Shell/ShellView.paml b/src/AvalonStudio.Shell/ShellView.paml index 6f8d5f9..dcf39d4 100644 --- a/src/AvalonStudio.Shell/ShellView.paml +++ b/src/AvalonStudio.Shell/ShellView.paml @@ -4,7 +4,6 @@ xmlns:menu="clr-namespace:AvalonStudio.Shell.Extensibility.MainMenu.Views;assembly=AvalonStudio.Shell.Extensibility" xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity" xmlns:ia="clr-namespace:Avalonia.Xaml.Interactions.Core;assembly=Avalonia.Xaml.Interactions" - xmlns:id="clr-namespace:Dock.Avalonia;assembly=Dock.Avalonia" xmlns:idc="clr-namespace:Dock.Avalonia.Controls;assembly=Dock.Avalonia" xmlns:beh="clr-namespace:AvalonStudio.Utils.Behaviors;assembly=AvalonStudio.Utils" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" @@ -24,20 +23,6 @@ - - - - - - - - - - - - @@ -80,10 +80,10 @@ @@ -91,10 +91,10 @@ @@ -102,14 +102,16 @@ From f1cb0602f239c45773eb3b554e120f840cdcd59a Mon Sep 17 00:00:00 2001 From: jcguo Date: Fri, 28 Aug 2026 13:59:20 +0800 Subject: [PATCH 3/8] Restore ShellExampleApp docking and toolbar icons --- src/AvalonStudio.Shell/Controls/ToolBar.paml | 6 +++++- src/AvalonStudio.Shell/Icons/Icons.xaml | 11 ++++++++++- src/AvalonStudio.Shell/ShellViewModel.cs | 2 +- src/ShellExampleApp/App.paml | 2 ++ src/ShellExampleApp/Commands/HelpCommands.cs | 2 +- 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/AvalonStudio.Shell/Controls/ToolBar.paml b/src/AvalonStudio.Shell/Controls/ToolBar.paml index 36a28b6..54c0bf7 100644 --- a/src/AvalonStudio.Shell/Controls/ToolBar.paml +++ b/src/AvalonStudio.Shell/Controls/ToolBar.paml @@ -40,7 +40,11 @@ diff --git a/src/AvalonStudio.Shell/Icons/Icons.xaml b/src/AvalonStudio.Shell/Icons/Icons.xaml index 99958fb..bd00f9f 100644 --- a/src/AvalonStudio.Shell/Icons/Icons.xaml +++ b/src/AvalonStudio.Shell/Icons/Icons.xaml @@ -21,6 +21,15 @@ Geometry="F1M9.5781,6.0029L9.1011,8.9999 8.5081,8.9999 8.5081,6.0559 8.5401,5.9999C9.6221,5.9999,9.4831,4.9999,9.4831,4.9999L6.5081,4.9999 6.5081,5.0719C6.5081,5.2949,6.5081,5.9999,7.5081,5.9999L7.5081,8.9999 6.9761,8.9999 6.8201,8.2889 6.4491,6.0029C5.7991,6.0029,5.5081,5.5719,5.5081,5.2739L5.5081,4.8749C5.5081,4.0769,6.4301,3.9999,6.4301,3.9999L9.5391,3.9999C9.5391,3.9999 10.5391,4.1249 10.5391,4.9999 10.5391,4.9999 10.5671,6.0029 9.5781,6.0029" /> + + + + + + + + + @@ -80,4 +89,4 @@ - \ No newline at end of file + diff --git a/src/AvalonStudio.Shell/ShellViewModel.cs b/src/AvalonStudio.Shell/ShellViewModel.cs index 6849823..cbb091f 100644 --- a/src/AvalonStudio.Shell/ShellViewModel.cs +++ b/src/AvalonStudio.Shell/ShellViewModel.cs @@ -190,7 +190,7 @@ public void SaveLayout() public StatusBarViewModel StatusBar => _statusBar.Value; - private ToolbarViewModel StandardToolbar { get; } + public ToolbarViewModel StandardToolbar { get; } public IEnumerable KeyBindings => _keyBindings; diff --git a/src/ShellExampleApp/App.paml b/src/ShellExampleApp/App.paml index a441e2e..34aa774 100644 --- a/src/ShellExampleApp/App.paml +++ b/src/ShellExampleApp/App.paml @@ -1,6 +1,7 @@  + diff --git a/src/ShellExampleApp/Commands/HelpCommands.cs b/src/ShellExampleApp/Commands/HelpCommands.cs index d2b6659..e423ac3 100644 --- a/src/ShellExampleApp/Commands/HelpCommands.cs +++ b/src/ShellExampleApp/Commands/HelpCommands.cs @@ -19,7 +19,7 @@ public HelpCommands(CommandIconService commandIconService) _shell = IoC.Get(); AboutCommand = new CommandDefinition( - "About", commandIconService.GetCompletionKindImage("Undo"), ReactiveCommand.Create(ShowAboutDialog)); + "About", commandIconService.GetCompletionKindImage("About"), ReactiveCommand.Create(ShowAboutDialog)); } private void ShowAboutDialog() From ce36953f5b58925a0081a222e6ebc8ca6837f523 Mon Sep 17 00:00:00 2001 From: jcguo Date: Fri, 28 Aug 2026 14:18:12 +0800 Subject: [PATCH 4/8] Add .NET 10 target support --- .../AvalonStudio.Shell.Extensibility.csproj | 3 +-- src/AvalonStudio.Shell/AvalonStudio.Shell.csproj | 3 +-- src/AvalonStudio.Utils/AvalonStudio.Utils.csproj | 2 +- src/ShellExampleApp/ShellExampleApp.csproj | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj b/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj index 3fa7dd9..252ddcf 100644 --- a/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj +++ b/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj @@ -1,7 +1,7 @@  - net8.0 + net8.0;net10.0 @@ -10,7 +10,6 @@ - diff --git a/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj b/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj index 8905c33..94a4aef 100644 --- a/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj +++ b/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj @@ -1,7 +1,7 @@  - net8.0 + net8.0;net10.0 @@ -11,7 +11,6 @@ - diff --git a/src/AvalonStudio.Utils/AvalonStudio.Utils.csproj b/src/AvalonStudio.Utils/AvalonStudio.Utils.csproj index 718694a..b5ca0aa 100644 --- a/src/AvalonStudio.Utils/AvalonStudio.Utils.csproj +++ b/src/AvalonStudio.Utils/AvalonStudio.Utils.csproj @@ -1,7 +1,7 @@  - net8.0 + net8.0;net10.0 diff --git a/src/ShellExampleApp/ShellExampleApp.csproj b/src/ShellExampleApp/ShellExampleApp.csproj index a534483..53d8cca 100644 --- a/src/ShellExampleApp/ShellExampleApp.csproj +++ b/src/ShellExampleApp/ShellExampleApp.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net8.0;net10.0 false From e87c12701d4adedc0a144f9c471e5251967d1813 Mon Sep 17 00:00:00 2001 From: jcguo Date: Fri, 28 Aug 2026 14:39:09 +0800 Subject: [PATCH 5/8] Upgrade Avalonia to version 12 --- .../AvalonStudio.Shell.Extensibility.csproj | 2 +- .../Controls/DocumentTabControl.xaml | 2 +- .../MainMenu/Views/MainMenuView.paml | 2 +- src/AvalonStudio.Shell/AvalonStudio.Shell.csproj | 6 ++---- src/AvalonStudio.Shell/Controls/MetroWindow.cs | 2 +- src/AvalonStudio.Shell/MenuShellView.paml | 4 ++-- src/AvalonStudio.Shell/Shell.cs | 4 ++-- src/AvalonStudio.Shell/ShellView.paml | 4 ++-- src/AvalonStudio.Utils/AvalonStudio.Utils.csproj | 4 +--- src/Directory.Build.props | 1 + src/Packages.targets | 7 ++----- src/ShellExampleApp/MainWindow.xaml | 2 +- src/Versions.props | 10 +++++----- 13 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj b/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj index 252ddcf..0e8f0a4 100644 --- a/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj +++ b/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.xaml b/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.xaml index f10eca1..20d56e9 100644 --- a/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.xaml +++ b/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.xaml @@ -10,7 +10,7 @@ diff --git a/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml b/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml index ea048b9..bb396f8 100644 --- a/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml +++ b/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml @@ -3,7 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="clr-namespace:AvalonStudio.Extensibility.Converters;assembly=AvalonStudio.Shell.Extensibility" xmlns:beh="clr-namespace:AvalonStudio.Shell.Extensibility.Behaviors;assembly=AvalonStudio.Shell.Extensibility" - xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity" + xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Xaml.Behaviors.Interactivity" xmlns:mod="clr-namespace:AvalonStudio.Menus.Models;assembly=AvalonStudio.Shell.Extensibility" xmlns:vmod="clr-namespace:AvalonStudio.Menus.ViewModels;assembly=AvalonStudio.Shell.Extensibility"> diff --git a/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj b/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj index 94a4aef..8ee7b4b 100644 --- a/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj +++ b/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj @@ -11,10 +11,8 @@ - - - - + + diff --git a/src/AvalonStudio.Shell/Controls/MetroWindow.cs b/src/AvalonStudio.Shell/Controls/MetroWindow.cs index d75fd69..968fdff 100644 --- a/src/AvalonStudio.Shell/Controls/MetroWindow.cs +++ b/src/AvalonStudio.Shell/Controls/MetroWindow.cs @@ -69,7 +69,7 @@ public MetroWindow() // The template supplies its own title bar and window controls. Keep // the platform title bar hidden so Windows does not render a second // minimise/maximise/close button set above it. - SystemDecorations = SystemDecorations.None; + WindowDecorations = Avalonia.Controls.WindowDecorations.None; if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { diff --git a/src/AvalonStudio.Shell/MenuShellView.paml b/src/AvalonStudio.Shell/MenuShellView.paml index b216d92..55e0b76 100644 --- a/src/AvalonStudio.Shell/MenuShellView.paml +++ b/src/AvalonStudio.Shell/MenuShellView.paml @@ -2,8 +2,8 @@ xmlns="https://github.com/avaloniaui" xmlns:cont="clr-namespace:AvalonStudio.Shell.Controls;assembly=AvalonStudio.Shell" xmlns:menu="clr-namespace:AvalonStudio.Shell.Extensibility.MainMenu.Views;assembly=AvalonStudio.Shell.Extensibility" - xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity" - xmlns:ia="clr-namespace:Avalonia.Xaml.Interactions.Core;assembly=Avalonia.Xaml.Interactions" + xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Xaml.Behaviors.Interactivity" + xmlns:ia="clr-namespace:Avalonia.Xaml.Interactions.Core;assembly=Xaml.Behaviors.Interactions" xmlns:id="clr-namespace:Dock.Avalonia;assembly=Dock.Avalonia" xmlns:idc="clr-namespace:Dock.Avalonia.Controls;assembly=Dock.Avalonia" xmlns:beh="clr-namespace:AvalonStudio.Utils.Behaviors;assembly=AvalonStudio.Utils" diff --git a/src/AvalonStudio.Shell/Shell.cs b/src/AvalonStudio.Shell/Shell.cs index bba5424..bf4af2f 100644 --- a/src/AvalonStudio.Shell/Shell.cs +++ b/src/AvalonStudio.Shell/Shell.cs @@ -1,6 +1,6 @@ using Avalonia; using Avalonia.Controls; -using Avalonia.ReactiveUI; +using ReactiveUI.Avalonia; using AvalonStudio.Extensibility; using AvalonStudio.Shell.Extensibility.Platforms; using Dock.Model.Core; @@ -14,7 +14,7 @@ public static class Shell public static void StartShellApp(this AppBuilder builder, string appName, ShellAppMainDelegate main, string[] args, IFactory layoutFactory = null) { builder - .UseReactiveUI() + .UseReactiveUI(_ => { }) .AfterSetup(_ => { Platform.AppName = appName; diff --git a/src/AvalonStudio.Shell/ShellView.paml b/src/AvalonStudio.Shell/ShellView.paml index dcf39d4..025ca2b 100644 --- a/src/AvalonStudio.Shell/ShellView.paml +++ b/src/AvalonStudio.Shell/ShellView.paml @@ -2,8 +2,8 @@ xmlns="https://github.com/avaloniaui" xmlns:cont="clr-namespace:AvalonStudio.Shell.Controls;assembly=AvalonStudio.Shell" xmlns:menu="clr-namespace:AvalonStudio.Shell.Extensibility.MainMenu.Views;assembly=AvalonStudio.Shell.Extensibility" - xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity" - xmlns:ia="clr-namespace:Avalonia.Xaml.Interactions.Core;assembly=Avalonia.Xaml.Interactions" + xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Xaml.Behaviors.Interactivity" + xmlns:ia="clr-namespace:Avalonia.Xaml.Interactions.Core;assembly=Xaml.Behaviors.Interactions" xmlns:idc="clr-namespace:Dock.Avalonia.Controls;assembly=Dock.Avalonia" xmlns:beh="clr-namespace:AvalonStudio.Utils.Behaviors;assembly=AvalonStudio.Utils" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" diff --git a/src/AvalonStudio.Utils/AvalonStudio.Utils.csproj b/src/AvalonStudio.Utils/AvalonStudio.Utils.csproj index b5ca0aa..bf6e1ce 100644 --- a/src/AvalonStudio.Utils/AvalonStudio.Utils.csproj +++ b/src/AvalonStudio.Utils/AvalonStudio.Utils.csproj @@ -6,9 +6,7 @@ - - - + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 6ee50b1..0ce61e5 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,6 +3,7 @@ Latest true + false diff --git a/src/Packages.targets b/src/Packages.targets index 94cd666..557f5cd 100644 --- a/src/Packages.targets +++ b/src/Packages.targets @@ -4,11 +4,8 @@ - - - - - + + diff --git a/src/ShellExampleApp/MainWindow.xaml b/src/ShellExampleApp/MainWindow.xaml index f05ac50..7832eeb 100644 --- a/src/ShellExampleApp/MainWindow.xaml +++ b/src/ShellExampleApp/MainWindow.xaml @@ -2,7 +2,7 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:cont="clr-namespace:AvalonStudio.Shell.Controls;assembly=AvalonStudio.Shell" - xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity" + xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Xaml.Behaviors.Interactivity" xmlns:id="clr-namespace:Dock.Avalonia;assembly=Dock.Avalonia" xmlns:shell="clr-namespace:AvalonStudio.Shell;assembly=AvalonStudio.Shell" xmlns:beh="clr-namespace:AvalonStudio.Utils.Behaviors;assembly=AvalonStudio.Utils" diff --git a/src/Versions.props b/src/Versions.props index 94b531e..bee0a61 100644 --- a/src/Versions.props +++ b/src/Versions.props @@ -1,12 +1,12 @@ - 11.3.0.6 - 11.3.11 - 11.3.9 - 11.3.11.22 + 12.1.1 + 12.1.1 + 12.0.7 + 12.1.0 13.0.3 - 20.4.1 + 24.1.0 10.0.11 10.0.11 9.0.0 From 865d27bbdb06fec2a182b5d46e00d8f110cb48f3 Mon Sep 17 00:00:00 2001 From: jcguo Date: Fri, 28 Aug 2026 16:34:47 +0800 Subject: [PATCH 6/8] Fix Azure build and upgrade Nuke GitVersion --- .nuke | 1 - .nuke/build.schema.json | 128 +++++++++++++++++++++++++++++++++++++++ .nuke/parameters.json | 4 ++ azure-pipelines.yml | 19 +++--- build/Build.cs | 30 ++++----- build/BuildParameters.cs | 21 ++++--- build/Shims.cs | 4 +- build/_build.csproj | 12 ++-- 8 files changed, 181 insertions(+), 38 deletions(-) delete mode 100644 .nuke create mode 100644 .nuke/build.schema.json create mode 100644 .nuke/parameters.json diff --git a/.nuke b/.nuke deleted file mode 100644 index 06ef971..0000000 --- a/.nuke +++ /dev/null @@ -1 +0,0 @@ -src/AvalonStudio.Shell.sln \ No newline at end of file diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 0000000..013e93e --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,128 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "Host": { + "type": "string", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string", + "enum": [ + "CiAzureWindows", + "Clean", + "Compile", + "CreateIntermediateNugetPackages", + "CreateNugetPackages", + "Restore" + ] + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { + "properties": { + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "description": "Host for execution. Default is 'automatic'", + "$ref": "#/definitions/Host" + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Verbosity": { + "description": "Logging verbosity during build execution. Default is 'Normal'", + "$ref": "#/definitions/Verbosity" + } + } + } + }, + "allOf": [ + { + "properties": { + "Configuration": { + "type": "string", + "description": "configuration" + }, + "ForceNugetVersion": { + "type": "string", + "description": "force-nuget-version" + }, + "SkipTests": { + "type": "boolean", + "description": "skip-tests" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + } + } + }, + { + "$ref": "#/definitions/NukeBuild" + } + ] +} diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 0000000..5d71eb1 --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "src/AvalonStudio.Shell.sln" +} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 908c2d9..ecce755 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,18 +13,23 @@ pool: vmImage: 'windows-latest' steps: - - task: CmdLine@2 - displayName: 'Install Nuke' + - task: UseDotNet@2 + displayName: 'Use .NET SDK 8' inputs: - script: | - dotnet tool install --global Nuke.GlobalTool --version 0.12.3 + packageType: 'sdk' + version: '8.0.x' + + - task: UseDotNet@2 + displayName: 'Use .NET SDK 10' + inputs: + packageType: 'sdk' + version: '10.0.x' - task: CmdLine@2 - displayName: 'Run Nuke' + displayName: 'Run build' inputs: script: | - set PATH=%PATH%;%USERPROFILE%\.dotnet\tools - nuke --target CiAzureWindows --configuration Release + dotnet run --project build\_build.csproj --configuration Release -- --target CiAzureWindows - task: PublishBuildArtifacts@1 inputs: diff --git a/build/Build.cs b/build/Build.cs index cd2f688..a263398 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -4,6 +4,7 @@ using Nuke.Common; using Nuke.Common.Execution; using Nuke.Common.Git; +using Nuke.Common.IO; using Nuke.Common.ProjectModel; using Nuke.Common.Tooling; using Nuke.Common.Tools.DotNet; @@ -76,14 +77,14 @@ void ExecWait(string preamble, string command, string args) .Before(Restore) .Executes(() => { - DeleteDirectories(Parameters.BuildDirs); - EnsureCleanDirectories(Parameters.BuildDirs); - EnsureCleanDirectory(Parameters.ArtifactsDir); - EnsureCleanDirectory(Parameters.NugetIntermediateRoot); - EnsureCleanDirectory(Parameters.NugetRoot); - EnsureCleanDirectory(Parameters.ZipRoot); - EnsureCleanDirectory(Parameters.TestResultsRoot); - EnsureCleanDirectory(OutputDirectory); + foreach (var directory in Parameters.BuildDirs) + directory.DeleteDirectory(); + Parameters.ArtifactsDir.CreateOrCleanDirectory(); + Parameters.NugetIntermediateRoot.CreateOrCleanDirectory(); + Parameters.NugetRoot.CreateOrCleanDirectory(); + Parameters.ZipRoot.CreateOrCleanDirectory(); + Parameters.TestResultsRoot.CreateOrCleanDirectory(); + OutputDirectory.CreateOrCleanDirectory(); }); Target Restore => _ => _ @@ -100,8 +101,8 @@ void ExecWait(string preamble, string command, string args) DotNetBuild(s => s .SetProjectFile(Solution) .SetConfiguration(Configuration) - .SetAssemblyVersion(GitVersion.GetNormalizedAssemblyVersion()) - .SetFileVersion(GitVersion.GetNormalizedFileVersion()) + .SetAssemblyVersion(GitVersion.AssemblySemVer) + .SetFileVersion(GitVersion.AssemblySemFileVer) .SetInformationalVersion(GitVersion.InformationalVersion) .EnableNoRestore()); }); @@ -110,10 +111,11 @@ void ExecWait(string preamble, string command, string args) .DependsOn(Compile) .Executes(() => { - EnsureCleanDirectory(Parameters.NugetIntermediateRoot); + Parameters.NugetIntermediateRoot.CreateOrCleanDirectory(); - DotNetPack(Solution, x => - x.SetConfiguration(Configuration) + DotNetPack(x => x + .SetProject(Solution) + .SetConfiguration(Configuration) .SetOutputDirectory(Parameters.NugetIntermediateRoot) .AddProperty("PackageVersion", Parameters.Version)); }); @@ -125,7 +127,7 @@ void ExecWait(string preamble, string command, string args) { var logger = new NumergeNukeLogger(); var config = Numerge.MergeConfiguration.LoadFile(RootDirectory / "build" / "numerge.config"); - EnsureCleanDirectory(Parameters.NugetRoot); + Parameters.NugetRoot.CreateOrCleanDirectory(); if (!Numerge.NugetPackageMerger.Merge(Parameters.NugetIntermediateRoot, Parameters.NugetRoot, config, logger)) throw new Exception("Package merge failed"); diff --git a/build/BuildParameters.cs b/build/BuildParameters.cs index d050856..0c0d455 100644 --- a/build/BuildParameters.cs +++ b/build/BuildParameters.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; using System.Xml.Linq; using Nuke.Common; -using Nuke.Common.BuildServers; +using Nuke.Common.CI.AzurePipelines; using Nuke.Common.Execution; using Nuke.Common.IO; using static Nuke.Common.IO.FileSystemTasks; @@ -53,7 +53,7 @@ public class BuildParameters public AbsolutePath BinRoot { get; } public AbsolutePath TestResultsRoot { get; } public string DirSuffix { get; } - public List BuildDirs { get; } + public List BuildDirs { get; } public string FileZipSuffix { get; } public AbsolutePath ZipCoreArtifacts { get; } public AbsolutePath ZipNuGetArtifacts { get; } @@ -75,19 +75,18 @@ public BuildParameters(Build b) MSBuildSolution = RootDirectory / "dirs.proj"; // PARAMETERS - IsLocalBuild = Host == HostType.Console; IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX; IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - IsRunningOnAzure = Host == HostType.TeamServices || - Environment.GetEnvironmentVariable("LOGNAME") == "vsts"; + IsRunningOnAzure = AzurePipelines.Instance is not null; + IsLocalBuild = !IsRunningOnAzure; 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, @@ -122,7 +121,9 @@ public BuildParameters(Build b) ZipRoot = ArtifactsDir / "zip"; BinRoot = ArtifactsDir / "bin"; TestResultsRoot = ArtifactsDir / "test-results"; - BuildDirs = GlobDirectories(RootDirectory, "**bin").Concat(GlobDirectories(RootDirectory, "**obj")).ToList(); + BuildDirs = RootDirectory.GlobDirectories("**/bin") + .Concat(RootDirectory.GlobDirectories("**/obj")) + .ToList(); DirSuffix = Configuration; FileZipSuffix = Version + ".zip"; ZipCoreArtifacts = ZipRoot / ("Avalonia-" + FileZipSuffix); diff --git a/build/Shims.cs b/build/Shims.cs index 461d617..0c472e5 100644 --- a/build/Shims.cs +++ b/build/Shims.cs @@ -19,9 +19,9 @@ static void Information(string info, params object[] args) 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; diff --git a/build/_build.csproj b/build/_build.csproj index 9d4629c..33038f7 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -2,7 +2,8 @@ Exe - net8.0 + net8.0 + true false False @@ -10,8 +11,11 @@ - - + + + + + @@ -22,7 +26,7 @@ - + From 392c877138c1e2ad2f151935b773308caca1fe7c Mon Sep 17 00:00:00 2001 From: jcguo Date: Fri, 28 Aug 2026 17:45:02 +0800 Subject: [PATCH 7/8] docs: map existing codebase --- .planning/codebase/ARCHITECTURE.md | 223 +++++++++++++++++++++++++++++ .planning/codebase/CONCERNS.md | 193 +++++++++++++++++++++++++ .planning/codebase/CONVENTIONS.md | 107 ++++++++++++++ .planning/codebase/INTEGRATIONS.md | 89 ++++++++++++ .planning/codebase/STACK.md | 88 ++++++++++++ .planning/codebase/STRUCTURE.md | 210 +++++++++++++++++++++++++++ .planning/codebase/TESTING.md | 117 +++++++++++++++ 7 files changed, 1027 insertions(+) create mode 100644 .planning/codebase/ARCHITECTURE.md create mode 100644 .planning/codebase/CONCERNS.md create mode 100644 .planning/codebase/CONVENTIONS.md create mode 100644 .planning/codebase/INTEGRATIONS.md create mode 100644 .planning/codebase/STACK.md create mode 100644 .planning/codebase/STRUCTURE.md create mode 100644 .planning/codebase/TESTING.md diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 0000000..59a519d --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -0,0 +1,223 @@ + +# Architecture + +**Analysis Date:** 2026-08-28 + +## System Overview + +```text +┌─────────────────────────────────────────────────────────────────────────┐ +│ Host application: `src/ShellExampleApp` │ +│ `App.paml.cs` → `MainWindow.xaml` → `ShellView.paml` │ +├─────────────────────────┬───────────────────────────┬───────────────────┤ +│ Extensibility contract │ Shell implementation │ Shared UI helpers │ +│ `AvalonStudio.Shell. │ `AvalonStudio.Shell` │ `AvalonStudio. │ +│ Extensibility` │ │ Utils` │ +└────────────┬────────────┴──────────────┬────────────┴───────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ MEF composition and extensibility │ +│ `CompositionRoot.cs`, `ExtensionManager.cs`, external `Extensions/` │ +└──────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Avalonia / ReactiveUI / Dock.Avalonia runtime and user settings │ +│ Application resources, Dock layout, `${BaseDirectory}/Settings/*.json` │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Component Responsibilities + +| Component | Responsibility | File | +|-----------|----------------|------| +| Desktop host | Creates the Avalonia application and supplies the main window/sample features. | `src/ShellExampleApp/App.paml.cs` | +| Shell bootstrap | Sets platform paths, builds MEF composition, initializes `IoC`, initializes the singleton shell, then starts the desktop lifetime. | `src/AvalonStudio.Shell/Shell.cs` | +| Extension contracts | Defines the public interfaces, metadata attributes, MVVM bases, UI controls, and cross-plugin service contracts. | `src/AvalonStudio.Shell.Extensibility/` | +| Shell workspace | Owns documents, perspectives, docking layout, key bindings, menu/toolbar view models, and modal-dialog state. | `src/AvalonStudio.Shell/ShellViewModel.cs` | +| Composition root | Combines already-loaded assemblies with assemblies declared in extension manifests into a MEF container. | `src/AvalonStudio.Shell/CompositionRoot.cs` | +| Extension discovery | Reads manifest files from the executable-relative `Extensions` directory. | `src/AvalonStudio.Shell/ExtensionManager.cs` | +| Docking adapter | Bridges domain document/tool view models to `Dock.Model` dockables and builds the default layout. | `src/AvalonStudio.Shell/DockExtensions.cs`, `src/AvalonStudio.Shell/Docking/` | +| Menus, toolbars, commands | Materializes MEF metadata exports into ordered UI models and Avalonia key bindings. | `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs`, `src/AvalonStudio.Shell.Extensibility/Toolbars/ToolbarService.cs`, `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs` | + +## Pattern Overview + +**Overall:** plugin-oriented desktop shell using Avalonia MVVM, MEF attribute composition, ReactiveUI state, and Dock.Avalonia workspace layout. + +**Key Characteristics:** + +- Put reusable, plugin-facing interfaces and attributes in `src/AvalonStudio.Shell.Extensibility`; keep concrete shell behavior in `src/AvalonStudio.Shell`. +- Use `[Export]`, `[Export(typeof(...))]`, `[ImportingConstructor]`, `[ImportMany]`, and `[Shared]` for service and feature discovery rather than manually constructing integrated features. +- Model document and tool content as `IDocumentTabViewModel` and `IToolViewModel`; adapt it to Dock.Avalonia only through `DockExtensions.Dock`. +- Resolve each view model's default view by replacing `ViewModel` with `View` in its fully-qualified type name through `ViewLocator.Build`. +- Keep application-specific sample features in `src/ShellExampleApp`; that project references the shell and extensibility projects rather than being referenced by them. + +## Layers + +**Host application layer:** + +- Purpose: starts Avalonia and selects the main window/application resources. +- Location: `src/ShellExampleApp/`. +- Contains: `App.paml.cs`, `MainWindow.xaml`, concrete document/tool view models, matching views, and sample command/menu/toolbar exports. +- Depends on: `AvalonStudio.Shell`, `AvalonStudio.Shell.Extensibility`, Avalonia desktop packages. +- Used by: end users running the `ShellExampleApp` executable. + +**Shell runtime layer:** + +- Purpose: orchestrates startup, MEF discovery, shell state, dock layout, shell controls, themes, and local extension-manifest parsing. +- Location: `src/AvalonStudio.Shell/`. +- Contains: `Shell.cs`, `ShellViewModel.cs`, `CompositionRoot.cs`, `Docking/`, `Controls/`, `Themes/`, and `Assets/`. +- Depends on: `AvalonStudio.Shell.Extensibility`, `AvalonStudio.Utils`, Avalonia, ReactiveUI, Dock.Avalonia, MEF. +- Used by: host applications and loaded extensions. + +**Extensibility layer:** + +- Purpose: provides stable contracts and base implementations for features loaded into a shell. +- Location: `src/AvalonStudio.Shell.Extensibility/`. +- Contains: `IExtension.cs`, `IoC.cs`, `Shell/IShell.cs`, `MVVM/`, `Documents/`, `Commands/`, `MainMenu/`, `Menus/`, `Toolbars/`, `Settings/`, and `Theme/`. +- Depends on: Avalonia, ReactiveUI, Dock.Avalonia, MEF, JSON serialization. +- Used by: the shell and application/third-party extensions. + +**Shared presentation utility layer:** + +- Purpose: supplies reusable Avalonia interaction behaviors that are not specific to the shell domain. +- Location: `src/AvalonStudio.Utils/Behaviors/`. +- Contains: focus, command, modal-window, and pointer-wheel behaviors. +- Depends on: Avalonia and Xaml.Behaviors. +- Used by: XAML in `src/AvalonStudio.Shell/` and `src/ShellExampleApp/`. + +## Data Flow + +### Primary startup and composition path + +1. `App.Main` invokes `BuildAvaloniaApp().StartShellApp(...)` (`src/ShellExampleApp/App.paml.cs:15`). +2. `Shell.StartShellApp` configures ReactiveUI, sets `Platform.AppName`, creates platform directories, discovers manifests, builds a MEF `CompositionHost`, and calls `IoC.Initialise` (`src/AvalonStudio.Shell/Shell.cs:14`). +3. `CompositionRoot.CreateContainer` composes all loaded assemblies plus each manifest's `mefComponents` assets (`src/AvalonStudio.Shell/CompositionRoot.cs:13`). +4. The shell resolves and initializes the MEF-exported `ShellViewModel`, activates `IActivatableExtension` implementations, and converts exported command gestures into `KeyBinding` objects (`src/AvalonStudio.Shell/ShellViewModel.cs:75`). +5. Avalonia creates `MainWindow`, whose view model resolves `IShell` from `IoC`, and `MainWindow.xaml` embeds `ShellView` and `StatusBar` (`src/ShellExampleApp/MainWindowViewModel.cs:8`, `src/ShellExampleApp/MainWindow.xaml`). + +### Extension discovery path + +1. `Platform.Initialise` ensures executable-relative `Extensions/` and user settings directories exist (`src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs:15`). +2. `ExtensionManager` enumerates direct child directories and loads each present `extension.json` manifest (`src/AvalonStudio.Shell/ExtensionManager.cs:22`). +3. `ExtensionManifest` exposes manifest asset groups, and `GetMefComponents` selects the `mefComponents` group (`src/AvalonStudio.Shell/ExtensionManifest.cs:42`, `src/AvalonStudio.Shell.Extensibility/ExtensionManifestExtensions.cs:7`). +4. The composition root loads those assemblies and MEF makes their exported services, tools, commands, and menu items available (`src/AvalonStudio.Shell/CompositionRoot.cs:29`). + +### Tool/document workspace path + +1. A feature resolves `IShell` and calls `AddOrSelectDocument` or `MainPerspective.AddOrSelectTool`; the extension helpers supply reuse-by-type behavior (`src/AvalonStudio.Shell.Extensibility/Shell/IShellExtensions.cs:9`). +2. `ShellViewModel.AddDocument` wraps document models as dockables using `DockExtensions.Dock`; `AvalonStudioPerspective.AddTool` creates or reuses location-specific tool docks (`src/AvalonStudio.Shell/ShellViewModel.cs:194`, `src/AvalonStudio.Shell/AvalonStudioPerspective.cs:55`). +3. `DefaultLayoutFactory` initializes the root layout and its `DocumentsPane`; `ShellView.paml` binds that layout to `DockControl` (`src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs:37`, `src/AvalonStudio.Shell/ShellView.paml`). +4. Dock selection propagates to `SelectedDocument` and document/tool lifecycle hooks through `ShellViewModel` and the MVVM base classes (`src/AvalonStudio.Shell/ShellViewModel.cs:166`, `src/AvalonStudio.Shell.Extensibility/Documents/GenericDocumentTabViewModel.cs:92`). + +### View resolution path + +1. Dock tab context is a `IDockableViewModel` (`src/AvalonStudio.Shell/DockExtensions.cs:18`). +2. `ShellExampleApp/App.paml` maps `IDockable` to `ViewModelViewHost` (`src/ShellExampleApp/App.paml`). +3. `ViewModelViewHost` calls `ViewLocator.Build(DataContext)` after its data context updates (`src/AvalonStudio.Shell.Extensibility/Controls/ViewModelViewHost.cs:68`). +4. `ViewLocator` converts a fully-qualified `*ViewModel` name to `*View`, looks in loaded assemblies, and creates a parameterless Avalonia `Control` (`src/AvalonStudio.Shell.Extensibility/MVVM/ViewLocator.cs:9`). + +**State Management:** + +- ReactiveUI properties (`RaiseAndSetIfChanged`, `WhenAnyValue`) back shell, document, tool, menu, and status state. +- `ShellViewModel.Instance` stores the initialized shell globally; `IoC` stores the MEF `CompositionHost` globally (`src/AvalonStudio.Shell/ShellViewModel.cs:31`, `src/AvalonStudio.Shell.Extensibility/IoC.cs:10`). +- Runtime preferences are serialized under `Platform.SettingsDirectory`; global settings are handled by `Settings`, while command and menu services expose their own settings models (`src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs:25`). + +## Key Abstractions + +**MEF extension:** + +- Purpose: a discoverable contribution to the host that may participate in pre-activation and activation. +- Examples: `src/AvalonStudio.Shell.Extensibility/IExtension.cs`, `src/ShellExampleApp/ConsoleViewModel.cs`. +- Pattern: export a type implementing `IExtension`; implement `IActivatableExtension` to add tools or perform setup during shell initialization. + +**Shell service:** + +- Purpose: feature-facing API for documents, perspectives, current selection, overlay, and modal dialogs. +- Examples: `src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs`, `src/AvalonStudio.Shell/ShellViewModel.cs`. +- Pattern: resolve `IShell` through `IoC.Get()`; do not instantiate `ShellViewModel` directly. + +**Dockable view model:** + +- Purpose: domain-level content that participates in tabbed documents or edge tool panes. +- Examples: `src/AvalonStudio.Shell.Extensibility/Documents/GenericDocumentTabViewModel.cs`, `src/AvalonStudio.Shell.Extensibility/MVVM/ToolViewModel.cs`. +- Pattern: subclass `DocumentTabViewModel` for central documents or `ToolViewModel` for an edge pane; give tools a `DefaultLocation` and pair the class with a parameterless `*View` control. + +**Metadata-driven UI contribution:** + +- Purpose: lets extensions declare commands and placement without the shell knowing feature types. +- Examples: `src/AvalonStudio.Shell.Extensibility/Commands/ExportCommandDefinitionAttribute.cs`, `src/AvalonStudio.Shell.Extensibility/MainMenu/ExportMainMenuItemAttribute.cs`, `src/AvalonStudio.Shell.Extensibility/Toolbars/ExportToolbarItemAttribute.cs`. +- Pattern: decorate MEF-exported members with export and ordering/group metadata; services materialize a deterministic view model. + +## Entry Points + +**Desktop executable:** + +- Location: `src/ShellExampleApp/App.paml.cs`. +- Triggers: operating-system process launch. +- Responsibilities: configures Avalonia, invokes the reusable shell bootstrap, restores theme settings, and creates `MainWindow`. + +**Reusable shell bootstrap:** + +- Location: `src/AvalonStudio.Shell/Shell.cs`. +- Triggers: host application's `App.Main`. +- Responsibilities: composes extensions and starts `StartWithClassicDesktopLifetime`. + +**Build and solution entry points:** + +- Location: `src/AvalonStudio.Shell.sln`, `build/Build.cs`, `src/build.ps1`, and `src/build.sh`. +- Triggers: developer and CI builds. +- Responsibilities: define solution projects, Nuke build targets, and platform-specific build entry scripts. + +## Architectural Constraints + +- **Threading:** Avalonia runs on a desktop UI thread; the sample defers theme resource changes through `Dispatcher.UIThread.InvokeAsync` in `src/ShellExampleApp/App.paml.cs`. +- **Global state:** `IoC` retains one `CompositionHost`; `ShellViewModel.Instance` retains one initialized shell; `Platform` caches the base directory; `Settings.Instance` caches global settings (`src/AvalonStudio.Shell.Extensibility/IoC.cs`, `src/AvalonStudio.Shell/ShellViewModel.cs`, `src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs`, `src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs`). +- **Assembly boundary:** dependency direction is `ShellExampleApp` → `AvalonStudio.Shell` → `AvalonStudio.Shell.Extensibility`/`AvalonStudio.Utils`; do not add references from the extensibility project to the concrete shell or app projects. +- **Circular imports:** no project-reference cycle is defined in `src/AvalonStudio.Shell.sln`; dynamic MEF loading can make runtime dependencies less explicit. +- **View naming:** dynamic view lookup requires the same namespace and a `ViewModel` → `View` name substitution plus a public parameterless `Control` constructor (`src/AvalonStudio.Shell.Extensibility/MVVM/ViewLocator.cs`). +- **Plugin location:** external manifests must be located in an immediate subdirectory of `Path.Combine(ExecutionPath, "Extensions")`, and declared assemblies must be in their manifest `mefComponents` asset group (`src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs:42`, `src/AvalonStudio.Shell/ExtensionManager.cs:25`). + +## Anti-Patterns + +### Direct shell construction + +**What happens:** A feature creates `ShellViewModel`, a dock layout, or a menu service itself. +**Why it's wrong:** It bypasses the initialized MEF composition, shared shell document list, key bindings, and current perspective. +**Do this instead:** Export the feature and resolve `IShell` through `IoC.Get()`, as in `src/ShellExampleApp/ConsoleViewModel.cs`. + +### Manually binding a dock tab to a view + +**What happens:** Feature code creates a Dock.Avalonia tab directly and separately assigns content. +**Why it's wrong:** Document/tool lifecycle hooks, selected-state synchronization, and type-based reuse stay outside shell tracking. +**Do this instead:** Create an `IDocumentTabViewModel` or `IToolViewModel` and use the `IShell`/`IPerspective` extension helpers in `src/AvalonStudio.Shell.Extensibility/Shell/IShellExtensions.cs`. + +### Placing reusable plugin contracts in the shell project + +**What happens:** New plugin interfaces, attributes, or generic view-model bases are added under `src/AvalonStudio.Shell/`. +**Why it's wrong:** External features would need a dependency on concrete runtime internals rather than the contract assembly. +**Do this instead:** Place public extension contracts under the relevant domain folder in `src/AvalonStudio.Shell.Extensibility/`. + +## Error Handling + +**Strategy:** plugin discovery and assembly loading isolate failures per extension; settings loading generally falls back to a default settings object. + +**Patterns:** + +- `ExtensionManager.LoadExtensions` catches malformed-manifest failures so one extension does not prevent enumeration (`src/AvalonStudio.Shell/ExtensionManager.cs:33`). +- `CompositionRoot.LoadMefComponents` catches individual assembly-load failures and writes the failure to standard output (`src/AvalonStudio.Shell/CompositionRoot.cs:39`). +- `IoC.Get` returns `default` when an export is unavailable; callers must treat a missing optional export as possible (`src/AvalonStudio.Shell.Extensibility/IoC.cs:35`). +- `Settings.Load` creates and saves default global settings when a persisted file is absent or cannot deserialize (`src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs:35`). + +## Cross-Cutting Concerns + +**Logging:** extension assembly failures use `System.Console.WriteLine` in `src/AvalonStudio.Shell/CompositionRoot.cs`; no centralized logging abstraction is present. + +**Validation:** manifest JSON requires `Name`, `Version`, and `Assets` through Newtonsoft.Json `JsonProperty(Required = Required.Always)` annotations in `src/AvalonStudio.Shell/ExtensionManifest.cs`. + +**Authentication:** not applicable; this is a local desktop shell with no authentication subsystem. + +--- + +*Architecture analysis: 2026-08-28* diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 0000000..c7c2fe0 --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -0,0 +1,193 @@ +# Codebase Concerns + +**Analysis Date:** 2026-08-28 + +## Tech Debt + +**Unimplemented collection-change cases:** +- Issue: `BindCollections` throws for `NotifyCollectionChangedAction.Move` and `Replace`, so a bound source collection cannot safely reorder or replace entries. +- Files: `src/AvalonStudio.Shell.Extensibility/MVVM/ObservableCollectionExtensions.cs:33` +- Impact: UI code using this helper fails at runtime when a source collection performs either standard `ObservableCollection` operation. +- Fix approach: Implement move and replace propagation, retain the event subscription in a disposable return value, and add tests covering add, move, replace, remove, reset, and disposal. + +**Disabled persistence features:** +- Issue: layout serialization is entirely commented out and `MenuSettingsService` always constructs new settings instead of reading its declared JSON file. +- Files: `src/AvalonStudio.Shell/ShellViewModel.cs:147`, `src/AvalonStudio.Shell/ShellViewModel.cs:183`, `src/AvalonStudio.Shell.Extensibility/Menus/Settings/MenuSettingsService.cs:34` +- Impact: dock layout and menu customizations do not survive an application restart, although save APIs and settings locations imply that they do. +- Fix approach: restore load/save paths with schema/version handling, validate deserialized data, and exercise restart persistence in integration tests. + +**Global service locators and mutable process state:** +- Issue: composition, shell access, settings, themes, platform configuration, and extension discovery depend on static mutable state. +- Files: `src/AvalonStudio.Shell.Extensibility/IoC.cs:10`, `src/AvalonStudio.Shell/ShellViewModel.cs:31`, `src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs:32`, `src/AvalonStudio.Shell.Extensibility/Theme/ColorTheme.cs:22`, `src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs:10`, `src/AvalonStudio.Shell/ExtensionManager.cs:16` +- Impact: startup order is implicit, tests cannot isolate application state, and multiple shell instances in one process share stale configuration and extension lists. +- Fix approach: pass dependencies through constructors, expose a scoped application host, and inject file-system/platform abstractions into services that currently reach static state. + +**Synchronous, unguarded settings I/O:** +- Issue: settings load occurs in the static `Settings` initializer and all settings save operations truncate and rewrite the target file directly without validation, recovery, synchronization, or atomic replacement. +- Files: `src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs:34`, `src/AvalonStudio.Shell.Extensibility/Utils/SerializedObject.cs:80`, `src/AvalonStudio.Shell.Extensibility/Commands/Settings/CommandSettingsService.cs:25`, `src/AvalonStudio.Shell.Extensibility/Menus/Settings/MenuSettingsService.cs:45` +- Impact: corrupt or partially written JSON can prevent startup or settings access; concurrent saves can lose updates; UI-thread saves can block on slow storage. +- Fix approach: load through exception-aware validation/migration, write to a same-directory temporary file then atomically replace, serialize access, and move I/O off the UI thread. + +**Unfinished Dock layout hooks:** +- Issue: the default layout factory subscribes to `VisibleDockables` and collection changes but both handlers contain no behavior; its dockable locator is also empty. +- Files: `src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs:82`, `src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs:91`, `src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs:137` +- Impact: Dock lifecycle behavior and restoration points are opaque, and a future persistence/restore implementation has no registered dockable factories. +- Fix approach: remove inert subscriptions or implement explicit lifecycle/persistence responsibilities with a tested registry of supported dockables. + +**Incomplete platform support:** +- Issue: custom title-bar support relies on a Windows `user32.dll` class-style mutation, while macOS behavior contains an explicitly unimplemented native title-bar path. +- Files: `src/AvalonStudio.Shell/Controls/MetroWindow.cs:32`, `src/AvalonStudio.Shell/Controls/MetroWindow.cs:74`, `src/AvalonStudio.Shell/Controls/MetroWindow.cs:94` +- Impact: window chrome behavior depends on Avalonia template internals and platform-specific native calls, increasing regression risk across Windows, Linux, macOS, and Avalonia upgrades. +- Fix approach: isolate platform-specific window APIs behind per-platform implementations and run UI smoke tests on all supported desktop targets. + +## Known Bugs + +**Dock view clone operation is not functional:** +- Symptoms: calling `HomeView.Clone()` throws `NotImplementedException`. +- Files: `src/AvalonStudio.Shell/Docking/Views/HomeView.cs:10` +- Trigger: any Dock operation that requests cloning of the `HomeView` dockable. +- Workaround: avoid clone-based Dock flows for `HomeView` until it returns a fully initialized dockable. + +**Document removal assumes an existing view mapping:** +- Symptoms: `RemoveDocument` indexes `_documentViews[document]` without checking for a mapping. +- Files: `src/AvalonStudio.Shell/ShellViewModel.cs:293` +- Trigger: remove a document that was not added successfully, was removed twice, or has a view-map inconsistency. +- Workaround: callers must only remove a document once after a successful `AddDocument`. + +**Command gesture access has an initialization-order failure:** +- Symptoms: `GetGesture` dereferences `_keyGestures` before `GetKeyGesture` initializes it. +- Files: `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs:45`, `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs:55` +- Trigger: an extension asks for a command gesture before shell initialization calls `GetKeyGesture`. +- Workaround: initialize command gestures before calling `GetGesture`. + +**Menu and command registration reject duplicate metadata at runtime:** +- Symptoms: immutable-dictionary builders use `Add`, which throws on duplicate command names; command activation also parses stored gesture strings without validation. +- Files: `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs:104`, `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs:117`, `src/AvalonStudio.Shell/ShellViewModel.cs:126` +- Trigger: installed extensions export duplicate command metadata names or a persisted command setting has an invalid key-gesture string. +- Workaround: maintain globally unique command names and repair the settings file before startup. + +**Declared `temporary` document behavior is ignored:** +- Symptoms: `AddDocument` accepts `temporary` but never reads it. +- Files: `src/AvalonStudio.Shell/ShellViewModel.cs:272` +- Trigger: callers supply `temporary: true` expecting preview/temporary-tab behavior. +- Workaround: do not rely on the parameter; model temporary state in the document implementation until the shell implements the lifecycle. + +## Security Considerations + +**Extension manifests can select arbitrary load paths:** +- Risk: a discovered extension manifest converts asset values to absolute paths without ensuring they remain under its extension directory, then MEF loads every declared component with `Assembly.LoadFrom`. +- Files: `src/AvalonStudio.Shell/ExtensionManifest.cs:54`, `src/AvalonStudio.Shell.Extensibility/ExtensionManifestExtensions.cs:9`, `src/AvalonStudio.Shell/CompositionRoot.cs:37`, `src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs:56` +- Current mitigation: extension folders are discovered only under `Extensions` beside the entry assembly, and individual load exceptions are caught. +- Recommendations: establish a trusted-extension policy; reject rooted and escaping paths after canonicalization; restrict loads to an allowed directory; verify publisher/signature or hashes before executing extension assemblies; log failures through the application logger rather than only standard output. + +**Package merger accepts unbounded archive input:** +- Risk: package bytes, every ZIP entry, and parsed XML are loaded into memory without entry-count, compressed-size, decompressed-size, or XML-complexity limits. +- Files: `Numerge/Numerge/LoadedPackage.cs:26`, `Numerge/Numerge/LoadedPackage.cs:50`, `Numerge/Numerge/LoadedPackage.cs:209`, `Numerge/Numerge/NugetPackageMerger.cs:26` +- Current mitigation: `NugetPackageMerger` reports only `MergeAbortedException` failures. +- Recommendations: preflight archive metadata, enforce configurable size/count/expansion limits, stream entries where possible, reject malformed package layouts, and convert expected ZIP/XML/I/O failures into reported merge errors. + +**Deserialization consumes user-writable files without containment or recovery controls:** +- Risk: global, command, menu, and extension configuration files are read directly from filesystem locations and malformed input is allowed to escape the loading paths. +- Files: `src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs:43`, `src/AvalonStudio.Shell.Extensibility/Commands/Settings/CommandSettingsService.cs:28`, `src/AvalonStudio.Shell/ExtensionManifest.cs:40`, `src/AvalonStudio.Shell.Extensibility/Utils/SerializedObject.cs:93` +- Current mitigation: default JSON settings omit null/default values and extension loading skips manifests that throw. +- Recommendations: treat persisted files as untrusted input, validate schema and maximum size, catch parse failures at every public load boundary, and back up/quarantine invalid settings instead of failing startup silently or globally. + +## Performance Bottlenecks + +**Unbounded in-memory package merge:** +- Problem: every `.nupkg` or `.snupkg` in the input directory is loaded as a full byte array, then every archive entry is copied into another byte array before merging. +- Files: `Numerge/Numerge/NugetPackageMerger.cs:26`, `Numerge/Numerge/LoadedPackage.cs:29`, `Numerge/Numerge/LoadedPackage.cs:34` +- Cause: the merge model uses `Dictionary` for all package content and keeps all packages resident until output writing completes. +- Improvement path: use temporary streams/files for large entries, process one merge group at a time, retain only metadata and selected binaries in memory, and impose package size limits. + +**Forced full garbage collection after each document removal:** +- Problem: document removal forces a process-wide `GC.Collect()`. +- Files: `src/AvalonStudio.Shell/ShellViewModel.cs:293`, `src/AvalonStudio.Shell/ShellViewModel.cs:314` +- Cause: reclamation is requested synchronously rather than allowing the runtime to schedule collection. +- Improvement path: remove the forced collection, identify retained Dock/event references with memory profiling, and dispose subscriptions or clear mappings explicitly when ownership ends. + +**Asynchronous collection mutations hide ordering and completion:** +- Problem: every mutation queues work with `Dispatcher.UIThread.InvokeAsync` and returns immediately, including range operations that enumerate caller-provided sequences later. +- Files: `src/AvalonStudio.Shell.Extensibility/MVVM/BindableCollection.cs:58`, `src/AvalonStudio.Shell.Extensibility/MVVM/BindableCollection.cs:166`, `src/AvalonStudio.Shell.Extensibility/MVVM/BindableCollection.cs:191` +- Cause: dispatch operations are not awaited or exposed to callers. +- Improvement path: expose task-returning mutation APIs or dispatch synchronously when already on the UI thread; snapshot input ranges before queueing; test ordering under background producers. + +## Fragile Areas + +**Template-part dependent window chrome:** +- Files: `src/AvalonStudio.Shell/Controls/MetroWindow.cs:139`, `src/AvalonStudio.Shell/Controls/MetroWindow.cs:213`, `src/AvalonStudio.Shell/Controls/MetroWindowTheme.paml.cs` +- Why fragile: pointer handling dereferences many template parts without null checks, and each template application adds new event handlers without detaching previous handlers. +- Safe modification: keep all required named parts synchronized with the window theme, guard optional parts, and attach/detach handlers through explicit lifecycle methods. +- Test coverage: no repository test project covers template reapplication, pointer resize paths, or platform-specific window chrome. + +**Shell/Dock state synchronization:** +- Files: `src/AvalonStudio.Shell/ShellViewModel.cs:147`, `src/AvalonStudio.Shell/ShellViewModel.cs:272`, `src/AvalonStudio.Shell/ShellViewModel.cs:323`, `src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs:19` +- Why fragile: documents are tracked in separate lists/dictionaries and Dock collections, perspectives are mutable, and navigation relies on casts and hard-coded dock IDs such as `DocumentsPane`. +- Safe modification: make one owner responsible for document lifecycle, validate required docks at layout initialization, and update all state stores atomically through tested commands. +- Test coverage: no repository test project exercises add, select, remove, perspective switch, or missing-dock failure paths. + +**NuGet package layout parser:** +- Files: `Numerge/Numerge/LoadedPackage.cs:40`, `Numerge/Numerge/LoadedPackage.cs:82`, `Numerge/Numerge/LoadedPackage.cs:212`, `Numerge/Numerge/LoadedPackage.cs:407` +- Why fragile: the parser assumes a `.nuspec`, `[Content_Types].xml`, grouped dependency XML, and `lib//` paths; malformed or valid alternative package layouts result in indexing, `First`, or attribute dereference exceptions. +- Safe modification: create a package-format validation layer, support direct `` nodes and content-type overrides, and return diagnostic errors instead of raw exceptions. +- Test coverage: no repository test project supplies package fixtures for valid, malformed, empty, multi-target, or symbol-package inputs. + +## Scaling Limits + +**Extension composition startup:** +- Current capacity: extension discovery scans every first-level directory and composition loads every MEF component assembly in the selected manifests during startup. +- Limit: startup time and memory grow with extension count and assembly dependency graphs; a failing component is reported only to standard output. +- Scaling path: cache validated manifests, load extensions lazily or in isolated load contexts, record structured diagnostics, and add startup telemetry around `src/AvalonStudio.Shell/ExtensionManager.cs:24` and `src/AvalonStudio.Shell/CompositionRoot.cs:31`. + +**Package-merger working set:** +- Current capacity: `Numerge` retains all discovered package contents in dictionaries until the merge finishes. +- Limit: the process working set scales with the combined uncompressed size of the input packages, not just the current output package. +- Scaling path: redesign `Numerge/Numerge/NugetPackageMerger.cs:21` and `Numerge/Numerge/LoadedPackage.cs:21` around bounded streaming and temporary storage. + +## Dependencies at Risk + +**Dual target framework maintenance:** +- Risk: every core project targets both `net8.0` and `net10.0`, while CI installs both SDKs and the build configuration has no test stage. +- Impact: incompatible dependency or Avalonia behavior can enter one target without automated runtime coverage. +- Migration plan: retain both targets only with a target-matrix test/build stage; otherwise define a supported LTS target policy in `src/AvalonStudio.Shell/AvalonStudio.Shell.csproj`, `src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj`, and `azure-pipelines.yml`. + +**NuGet v2 package source:** +- Risk: restore configuration points to the NuGet v2 endpoint. +- Impact: package restore remains dependent on the legacy protocol endpoint and lacks a repository-level locked-mode policy. +- Migration plan: move to the v3 service index and add lock files or a centrally enforced restore policy in `src/nuget.config` and the project build configuration. + +## Missing Critical Features + +**Extension lifecycle isolation and diagnostics:** +- Problem: extension discovery swallows manifest errors and extension component load failures only write to standard output; loaded extensions execute inside the host process with no version compatibility or trust checks. +- Blocks: reliable support triage, safe third-party extension distribution, and predictable recovery from extension faults. +- Files: `src/AvalonStudio.Shell/ExtensionManager.cs:20`, `src/AvalonStudio.Shell/CompositionRoot.cs:31`, `src/AvalonStudio.Shell.Extensibility/Utils/AppDomain.cs:18` + +**Package merger format compatibility layer:** +- Problem: the merger implements only narrow `netstandard2.0` fallback handling and preserves only simplified content-type/dependency structures. +- Blocks: dependable merging of modern multi-target NuGet packages and clear compatibility diagnostics. +- Files: `Numerge/Numerge/LoadedPackage.cs:95`, `Numerge/Numerge/LoadedPackage.cs:125`, `Numerge/Numerge/LoadedPackage.cs:403` + +## Test Coverage Gaps + +**Repository-wide automated tests:** +- What's not tested: no committed test project, test source files, or test-runner package references cover the shell, extensibility library, utilities, or `Numerge`. +- Files: `src/AvalonStudio.Shell.sln`, `src/AvalonStudio.Shell/AvalonStudio.Shell.csproj`, `Numerge/Numerge.sln`, `azure-pipelines.yml` +- Risk: regressions in desktop behavior, settings migrations, extension loading, and NuGet package output are detected only through manual builds or consumers. +- Priority: High. + +**Persistence and malformed-input recovery:** +- What's not tested: corrupted JSON, concurrent writes, partial writes, invalid key gestures, invalid extension manifests, malformed ZIP/XML, and unsupported package layouts. +- Files: `src/AvalonStudio.Shell.Extensibility/Utils/SerializedObject.cs`, `src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs`, `src/AvalonStudio.Shell/ExtensionManifest.cs`, `Numerge/Numerge/LoadedPackage.cs` +- Risk: startup and package processing can fail with unhandled exceptions or silently discard diagnostics. +- Priority: High. + +**Cross-platform desktop behavior:** +- What's not tested: title-bar template resolution, resize/move gestures, Windows native-handle behavior, macOS decoration behavior, and Linux shell startup. +- Files: `src/AvalonStudio.Shell/Controls/MetroWindow.cs`, `src/AvalonStudio.Shell/Shell.cs`, `azure-pipelines.yml` +- Risk: platform-specific regressions can ship because CI runs only `windows-latest`. +- Priority: High. + +--- + +*Concerns audit: 2026-08-28* diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 0000000..b884c25 --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -0,0 +1,107 @@ +# Coding Conventions + +**Analysis Date:** 2026-08-28 + +## Naming Patterns + +**Files:** +- Use PascalCase C# filenames that name the primary type: `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs`, `src/AvalonStudio.Shell/Controls/StatusBarViewModel.cs`, and `src/ShellExampleApp/MainWindowViewModel.cs`. +- Pair Avalonia views with their code-behind using the same basename (for example, `src/ShellExampleApp/MainWindow.xaml` and `src/ShellExampleApp/MainWindow.xaml.cs`). Several reusable shell resources use the legacy `.paml.cs` suffix, such as `src/AvalonStudio.Shell/ShellView.paml.cs` and `src/AvalonStudio.Shell/Controls/StatusBar.paml.cs`; retain the existing suffix when extending those resources. +- Place types in a directory corresponding to their concern and namespace segment, such as `MVVM`, `Commands`, `Menus`, `Docking`, `Converters`, or `Behaviors` beneath `src/AvalonStudio.Shell.Extensibility/`. + +**Functions:** +- Use PascalCase for public, protected, and private methods (`GetMainMenu`, `ResolveCommands`, `LoadLayout`) in `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs` and `src/AvalonStudio.Shell/ShellViewModel.cs`. +- Use `On...` for lifecycle hooks and event-style methods, including `OnOpen`, `OnClose`, `OnSelected`, and `OnFrameworkInitializationCompleted` in `src/AvalonStudio.Shell.Extensibility/Documents/GenericDocumentTabViewModel.cs` and `src/ShellExampleApp/App.paml.cs`. +- Use verb-led extension methods such as `Dock`, `AddOrSelectDocument`, and `GetOrCreate` in `src/AvalonStudio.Shell/DockExtensions.cs` and `src/AvalonStudio.Shell.Extensibility/Shell/IShellExtensions.cs`. + +**Variables:** +- Use camelCase for parameters, local variables, and pattern variables (`commandSettings`, `parentItem`, `activatable`) throughout `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs`. +- Use private camelCase fields in most UI/view-model code (`_documents`, `_viewModel`, `modalDialog`) as in `src/AvalonStudio.Shell/ShellViewModel.cs`; legacy and binding-oriented classes also use non-underscore fields (`lineNumber`, `isVisible`) in `src/AvalonStudio.Shell/Controls/StatusBarViewModel.cs` and `src/AvalonStudio.Shell.Extensibility/Dialogs/ModalDialogViewModelBase.cs`. Follow the surrounding file's field style instead of mixing both conventions. +- Use the `s_` prefix for private static mutable fields, as in `s_compositionHost` in `src/AvalonStudio.Shell.Extensibility/IoC.cs`. + +**Types:** +- Use PascalCase for classes, interfaces, enums, attributes, and generic parameters. Prefix interfaces with `I` (`IShell`, `IMenuItemFactory`, `IObservableCollection`) in `src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs`, `src/AvalonStudio.Shell.Extensibility/Menus/IMenuItemFactory.cs`, and `src/AvalonStudio.Shell.Extensibility/MVVM/IObservableCollection.cs`. +- Name UI state classes with `ViewModel`, controls/views with `View`, and MEF attribute types with `Export...Attribute`, matching `src/AvalonStudio.Shell/Controls/StatusBarViewModel.cs` and `src/AvalonStudio.Shell.Extensibility/Commands/ExportCommandDefinitionAttribute.cs`. + +## Code Style + +**Formatting:** +- No repository-root formatter configuration is detected. The only editor configuration is `build/.editorconfig`, so it applies to the build project rather than the `src/` projects. +- In `build/.editorconfig`, omit redundant `this.` qualification and prefer expression-bodied properties, indexers, and accessors. It explicitly permits omitted accessibility modifiers (`dotnet_style_require_accessibility_modifiers = never`). +- Use block-scoped namespaces. Every production C# sample uses `namespace ... { ... }`, for example `src/AvalonStudio.Shell/CompositionRoot.cs` and `Numerge/Numerge/NugetPackageMerger.cs`. +- Place opening braces on a new line. Preserve the local indentation of the file: the repository contains both spaces and tabs, notably tabs in `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs` and spaces in `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs`. +- Prefer concise expression-bodied members where already established (`Title => Platform.AppName` in `src/AvalonStudio.Shell/ShellViewModel.cs` and `BuildAvaloniaApp() => ...` in `src/ShellExampleApp/App.paml.cs`); use full accessors when setters contain notification or guard logic. + +**Linting:** +- No project-level analyzer, lint task, `TreatWarningsAsErrors`, `Nullable`, or `LangVersion` configuration is detected in the `.csproj` files or CI pipeline. +- `build/.editorconfig` sets the listed style diagnostics to warning severity. Do not assume additional analyzer enforcement. + +## Import Organization + +**Order:** +1. Avalonia, Dock, ReactiveUI, and other framework imports are commonly placed first. +2. `AvalonStudio.*` project imports generally follow. +3. `System.*` imports occur last in many UI files, including `src/AvalonStudio.Shell/ShellViewModel.cs` and `src/ShellExampleApp/Commands/ThemeCommands.cs`. + +- Import ordering is not mechanically consistent: `src/AvalonStudio.Shell/CompositionRoot.cs` starts with project imports while `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs` starts with `System.*`. Keep imports explicit and preserve the grouping/order of the file being modified; do not introduce a new sorting rule in an isolated change. +- Use direct namespaces rather than a repository-wide global-using mechanism. Static imports are limited to build orchestration in `build/Build.cs`. + +**Path Aliases:** +- Not applicable. C# projects reference namespaces directly; no alias/import configuration is detected. + +## Error Handling + +**Patterns:** +- Let ordinary framework and programming errors propagate. Most public methods do not wrap exceptions, including layout and menu construction in `src/AvalonStudio.Shell/ShellViewModel.cs` and `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs`. +- Catch only a failure with a defined recovery result. `Numerge/Numerge/NugetPackageMerger.cs` catches `MergeAbortedException`, logs its message, and returns `false`; callers in `build/Build.cs` convert `false` to a build failure. +- At extension-discovery boundaries, catch broad load errors and continue loading remaining components, emitting diagnostic text through `System.Console.WriteLine` as in `src/AvalonStudio.Shell/CompositionRoot.cs`. +- Service lookup treats unavailable optional exports as absence: `src/AvalonStudio.Shell.Extensibility/IoC.cs` returns `default` for single lookups and `Enumerable.Empty()` for plural lookup. Callers must safely handle a missing export. +- Use early null guards for optional arguments, as in `RemoveDocument` in `src/AvalonStudio.Shell/ShellViewModel.cs`; avoid adding broad catch-and-ignore blocks. + +## Logging + +**Framework:** console and Nuke build logging; no application logging abstraction is detected. + +**Patterns:** +- Use `Information(...)` for build diagnostics in `build/Build.cs`. +- `Numerge/Numerge/INumergeLogger.cs` defines `Info`, `Error`, and `Warning` for package-merge work; pass an `INumergeLogger` into merger operations rather than writing directly from that library. +- Use `System.Console.WriteLine` only for extension-loading diagnostics in `src/AvalonStudio.Shell/CompositionRoot.cs`. No structured logging provider is configured for shell runtime code. + +## Comments + +**When to Comment:** +- Comment non-obvious platform, plugin, or framework constraints. `src/AvalonStudio.Shell/CompositionRoot.cs` documents why its custom `AppDomain` loader remains, and `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs` explains the menu-parent registration rule. +- Preserve targeted suppression comments where required, such as the ReSharper suppressions at the top of `Numerge/Numerge/NugetPackageMerger.cs`; do not add blanket suppression files. +- TODO comments exist for explicit pending work (for example `src/AvalonStudio.Shell/CompositionRoot.cs` and `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs`). Keep a TODO adjacent to the constrained behavior and describe the intended outcome. + +**JSDoc/TSDoc:** +- Use XML documentation for reusable public/protected collection and framework-extension behavior. `src/AvalonStudio.Shell.Extensibility/MVVM/BindableCollection.cs` documents overridden collection operations and their parameters with `/// ` and `/// `. +- Small view models, services, and straightforward public models generally have no XML comments; avoid redundant documentation for self-evident members. + +## Function Design + +**Size:** +- Small helpers and model members are preferred, but orchestration classes may own longer stateful flows: `src/AvalonStudio.Shell/ShellViewModel.cs` manages shell lifecycle and docking, and `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs` builds menu trees. Keep UI state transitions with the owning view model/service rather than creating ad-hoc utility classes. + +**Parameters:** +- Use constructor injection with `[ImportingConstructor]` for MEF-composed services (`src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs` and `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs`). +- Use optional parameters for feature toggles/default UI behavior (`AddDocument(..., bool temporary = false, bool select = true)` in `src/AvalonStudio.Shell/ShellViewModel.cs`) and generic constraints for document/type factories in the same file. + +**Return Values:** +- Return immutable/read-only views of owned mutable state when exposing collections (`Documents => _documents.AsReadOnly()` in `src/AvalonStudio.Shell/ShellViewModel.cs`). +- Return `bool` for expected operation success/failure in the Numerge merge layer (`Numerge/Numerge/NugetPackageMerger.cs`); use exceptions for unrecoverable build failure in `build/Build.cs`. +- For reactive UI properties, update backing fields through `RaiseAndSetIfChanged` or explicitly raise dependent property notifications, following `src/AvalonStudio.Shell/Controls/StatusBarViewModel.cs`. + +## Module Design + +**Exports:** +- Use one primary public type per PascalCase file. Keep closely coupled helpers/implementations together when they are internal, such as the tab classes in `src/AvalonStudio.Shell/DockExtensions.cs`. +- Expose extension points through interfaces and MEF attributes: `src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs`, `src/AvalonStudio.Shell.Extensibility/Commands/ExportCommandDefinitionAttribute.cs`, and `[Export]/[Shared]` services in `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs`. +- Use `internal` for example-app registration holders that should not form public API, as in `src/ShellExampleApp/Commands/ThemeCommands.cs` and `src/ShellExampleApp/MainMenu/ThemeMainMenuItem.cs`. + +**Barrel Files:** +- Not applicable. No C# barrel/export-aggregator files are detected; consumers import the concrete namespace/type they need. + +--- + +*Convention analysis: 2026-08-28* diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 0000000..ec20c4a --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -0,0 +1,89 @@ +# External Integrations + +**Analysis Date:** 2026-08-28 + +## APIs & External Services + +**Package distribution:** +- NuGet.org - package restore source for the .NET solution. + - SDK/Client: .NET SDK/NuGet invoked by `DotNetRestore` in `build/Build.cs`. + - Auth: not configured in `src/nuget.config`; it contains only the public `nuget.org` source. + +**Source control and versioning:** +- Git/GitHub repository metadata - NUKE identifies the canonical repository and uses GitVersion metadata during builds. + - SDK/Client: `Nuke.Common` 8.1.4 and `GitVersion.Tool` 6.8.2 in `build/_build.csproj`. + - Auth: not handled by application code; CI checkout credentials are platform-managed. + +**CI service:** +- Azure Pipelines - builds every branch/tag on `windows-latest`, installs .NET SDK 8 and 10, runs the NUKE target, and publishes NuGet artifacts. + - SDK/Client: Azure Pipelines YAML tasks in `azure-pipelines.yml`; CI-state detection via `Nuke.Common.CI.AzurePipelines` in `build/BuildParameters.cs`. + - Auth: Azure Pipelines managed identity/token configuration is not defined in this repository. + +**Desktop extension boundary:** +- Local MEF extensions - the application discovers `extension.json` manifests under the executable's `Extensions` directory, then loads declared `mefComponents` assemblies. + - SDK/Client: `System.Composition` in `src/AvalonStudio.Shell/CompositionRoot.cs` and `src/AvalonStudio.Shell/ExtensionManager.cs`. + - Auth: none; extensions are filesystem-local rather than a remote API. + +No HTTP clients, third-party SaaS SDKs, payment services, messaging providers, or outbound application API calls are detected in `src/`. + +## Data Storage + +**Databases:** +- Not detected. No ORM, database client, connection string, or database configuration is present in `src/`. + +**File Storage:** +- Local filesystem only. The platform initialises a per-application base directory, `Settings` directory, and executable-relative `Extensions` directory in `src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs`. +- `GlobalSettings.json` is read and written via `src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs`. +- `CommandSettings.json` is read and written via `src/AvalonStudio.Shell.Extensibility/Commands/Settings/CommandSettingsService.cs`. +- Extension manifests are parsed from local `extension.json` files by `src/AvalonStudio.Shell/ExtensionManifest.cs`. +- Client: `Newtonsoft.Json` 13.0.3 through the shared serializer in `src/AvalonStudio.Shell.Extensibility/Utils/SerializedObject.cs`. + +**Caching:** +- Local application storage only. `AVALON_CACHE_PATH` can relocate the base directory; no distributed cache is configured. + +## Authentication & Identity + +**Auth Provider:** +- Not detected. `src/` contains no authentication, authorization, OAuth/OpenID, JWT, or identity-provider integration. + - Implementation: not applicable. + +## Monitoring & Observability + +**Error Tracking:** +- None detected. No error-tracking SDK or telemetry service is referenced by the project files or source. + +**Logs:** +- The extension loader writes component-load failures to standard console output in `src/AvalonStudio.Shell/CompositionRoot.cs`. +- NUKE writes build diagnostics through its logging API in `build/Build.cs`; no structured or remote log sink is configured. + +## CI/CD & Deployment + +**Hosting:** +- Not applicable. This is a desktop-library and NuGet-package repository, not a hosted service. + +**CI Pipeline:** +- Azure Pipelines configuration in `azure-pipelines.yml` executes `CiAzureWindows` from `build/Build.cs` and uploads `artifacts/nuget` as a pipeline artifact. +- Packaging is local to the build agent: `build/Build.cs` restores, compiles, packs, and merges packages. No NuGet feed push/publish task is configured. + +## Environment Configuration + +**Required env vars:** +- None for normal application startup. +- Optional `AVALON_CACHE_PATH` overrides the runtime settings base path in `src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs`. +- Azure builds expect the non-secret `BUILD_BUILDID` CI variable when generating CI prerelease versions in `build/BuildParameters.cs`. + +**Secrets location:** +- No repository-managed secret file or secret-store integration is detected. No `.env` file is present. +- CI and source-control authentication, if needed, remain external to repository code and configuration. + +## Webhooks & Callbacks + +**Incoming:** +- None detected. The repository contains no web server, endpoint, or webhook handler. + +**Outgoing:** +- None detected from application code. The CI pipeline publishes artifacts to Azure Pipelines; it does not define application callbacks. + +--- + +*Integration audit: 2026-08-28* diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 0000000..b401903 --- /dev/null +++ b/.planning/codebase/STACK.md @@ -0,0 +1,88 @@ +# Technology Stack + +**Analysis Date:** 2026-08-28 + +## Languages + +**Primary:** +- C# (`LangVersion` `Latest`) - application libraries, the `ShellExampleApp` desktop executable, and the NUKE build code in `src/` and `build/`. +- Avalonia XAML markup - desktop views and resources in `src/AvalonStudio.Shell/**/*.xaml`, `src/AvalonStudio.Shell.Extensibility/**/*.xaml`, and `src/ShellExampleApp/**/*.xaml`. + +**Secondary:** +- MSBuild XML - project, dependency, and version configuration in `src/*.csproj`, `src/Directory.Build.props`, `src/Versions.props`, `src/Packages.targets`, and `build/SharedVersion.props`. +- JSON - NUKE build metadata in `.nuke/parameters.json` and local extension/settings payloads managed by `src/AvalonStudio.Shell/ExtensionManifest.cs` and `src/AvalonStudio.Shell.Extensibility/Utils/SerializedObject.cs`. +- YAML - Azure Pipelines CI definition in `azure-pipelines.yml`. +- PowerShell and Bash - cross-platform build bootstrap scripts in `src/build.ps1` and `src/build.sh`; Linux launcher in `src/AvalonStudio.Shell/avalonstudio.sh`. + +## Runtime + +**Environment:** +- .NET SDK 8.0 and 10.0 - explicitly installed by `azure-pipelines.yml`. +- .NET targets - the reusable shell projects and sample app target `net8.0;net10.0` in `src/AvalonStudio.Shell/AvalonStudio.Shell.csproj`, `src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj`, `src/AvalonStudio.Utils/AvalonStudio.Utils.csproj`, and `src/ShellExampleApp/ShellExampleApp.csproj`. +- Build tooling targets `net8.0` in `build/_build.csproj`; its bundled `Numerge` utility targets `netstandard2.0` in `Numerge/Numerge/Numerge.csproj`. +- Desktop runtime - Avalonia platform detection is started by `src/ShellExampleApp/App.paml.cs`; Windows, Linux, and macOS are handled in `src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs`. + +**Package Manager:** +- NuGet through the .NET SDK/MSBuild. The repository source is defined in `src/nuget.config` as `nuget.org`. +- Lockfile: missing (`packages.lock.json` is not present); restore resolves dependencies from project and central MSBuild version files. + +## Frameworks + +**Core:** +- Avalonia 12.1.1 - cross-platform desktop UI, resources, and platform integration. Version is centralized in `src/Versions.props`; desktop references are in `src/AvalonStudio.Shell/AvalonStudio.Shell.csproj` and `src/ShellExampleApp/ShellExampleApp.csproj`. +- ReactiveUI 24.1.0 and ReactiveUI.Avalonia 12.1.1 - reactive MVVM support in the shell and extensibility projects; versions are centralized in `src/Versions.props`. +- Dock.Avalonia 12.1.0 - docking layout model and UI in `src/AvalonStudio.Shell/AvalonStudio.Shell.csproj`; the sample app uses the Fluent docking theme in `src/ShellExampleApp/ShellExampleApp.csproj`. +- System.Composition 10.0.11 - MEF-style extension discovery and composition in `src/AvalonStudio.Shell/CompositionRoot.cs` and export attributes throughout `src/AvalonStudio.Shell.Extensibility/`. + +**Testing:** +- Not detected. No test projects, test-framework references, or test configuration files are present. + +**Build/Dev:** +- MSBuild/.NET SDK - project compilation and packaging, with common properties imported by `src/Directory.Build.props` and package versions applied by `src/Directory.Build.targets`. +- NUKE 8.1.4 - build orchestration in `build/_build.csproj` and `build/Build.cs`. +- GitVersion.Tool 6.8.2 - build-time version metadata downloaded by `build/_build.csproj` and consumed by `build/Build.cs`. +- Numerge - repository-local NuGet package merger referenced by `build/_build.csproj` and configured by `build/numerge.config`. + +## Key Dependencies + +**Critical:** +- `Avalonia`, `Avalonia.Desktop`, and `Avalonia.Themes.Fluent` 12.1.1 - UI framework, desktop host, and Fluent styling; version declarations are in `src/Versions.props` and package use is in the `src/*.csproj` files. +- `ReactiveUI` 24.1.0 and `ReactiveUI.Avalonia` 12.1.1 - bindings and view-model behavior used across `src/AvalonStudio.Shell/` and `src/AvalonStudio.Shell.Extensibility/`. +- `Dock.Avalonia`, `Dock.Model.ReactiveUI`, and Dock theme packages 12.1.0 - docking UI, models, and themes configured through `src/Packages.targets`. +- `Newtonsoft.Json` 13.0.3 - extension manifest and settings serialization in `src/AvalonStudio.Shell/ExtensionManifest.cs` and `src/AvalonStudio.Shell.Extensibility/Utils/SerializedObject.cs`. +- `Microsoft.Extensions.DependencyModel` 10.0.11 - runtime dependency inspection for MEF assembly loading in `src/AvalonStudio.Shell.Extensibility/Utils/AppDomain.cs`. + +**Infrastructure:** +- `System.Composition` and `System.Composition.AttributedModel` 10.0.11 - plugin exports and the composition host in `src/AvalonStudio.Shell/CompositionRoot.cs`. +- `System.Reactive` 7.0.0 and `System.Collections.Immutable` 9.0.0 - reactive streams and immutable collections referenced by the shell projects. +- `Xaml.Behaviors.Avalonia` 12.0.7 - reusable UI behaviors in `src/AvalonStudio.Shell/Behaviors/` and `src/AvalonStudio.Utils/Behaviors/`. + +## Configuration + +**Environment:** +- No `.env` files or application secret configuration were detected. +- `AVALON_CACHE_PATH` is an optional runtime override for the application base directory in `src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs`. Without it, the desktop host uses `%UserProfile%/{AppName}` on Windows or `$HOME/{AppName}` on Unix-like platforms. +- `BUILD_BUILDID` is read only while an Azure build derives a CI prerelease version in `build/BuildParameters.cs`. +- Local configuration is JSON persisted under the platform settings directory by `src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs` and `src/AvalonStudio.Shell.Extensibility/Commands/Settings/CommandSettingsService.cs`. + +**Build:** +- Central language and deterministic-build configuration: `src/Directory.Build.props`. +- Central dependency versions: `src/Versions.props`; common package-version updates: `src/Packages.targets` imported by `src/Directory.Build.targets`. +- Product metadata and package version baseline: `build/SharedVersion.props`. +- NUKE build parameters and selected solution: `.nuke/parameters.json`; build targets: `build/Build.cs`. +- CI configuration: `azure-pipelines.yml`; NuGet source configuration: `src/nuget.config`. + +## Platform Requirements + +**Development:** +- Install .NET SDK 8 and .NET SDK 10 to match `azure-pipelines.yml`, then restore/build `src/AvalonStudio.Shell.sln` with `dotnet`. +- Use `dotnet run --project build/_build.csproj -- --target CiAzureWindows` to execute the CI-equivalent NUKE target, as defined in `azure-pipelines.yml`. +- Windows, Linux, and macOS desktop development are supported by Avalonia platform detection in `src/ShellExampleApp/App.paml.cs`; the Linux launcher invokes Mono/Skia in `src/AvalonStudio.Shell/avalonstudio.sh`. + +**Production:** +- Distributable output is reusable `net8.0`/`net10.0` .NET libraries plus the `ShellExampleApp` desktop example. `build/Build.cs` packages the solution and merges NuGet outputs into `artifacts/nuget`. +- No server, container, or web-host deployment target is configured. + +--- + +*Stack analysis: 2026-08-28* diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 0000000..cedff85 --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -0,0 +1,210 @@ +# Codebase Structure + +**Analysis Date:** 2026-08-28 + +## Directory Layout + +```text +AvalonStudio.Shell/ +├── src/ # Main solution and product source +│ ├── AvalonStudio.Shell.Extensibility/ # Public plugin contracts and reusable UI/MVVM APIs +│ ├── AvalonStudio.Shell/ # Concrete shell runtime, controls, docking, themes +│ ├── AvalonStudio.Utils/ # Generic Avalonia behavior helpers +│ ├── ShellExampleApp/ # Executable sample host and sample contributions +│ ├── AvalonStudio.Shell.sln # Visual Studio solution +│ ├── Directory.Build.props # Source-wide compiler settings +│ └── Versions.props # Central package versions +├── build/ # Nuke build project and shared build/version props +├── Numerge/ # NuGet package merge utility used by the build +├── .nuke/ # Nuke generated/build parameter metadata +├── azure-pipelines.yml # Azure Pipelines definition +└── README.md # Project description +``` + +## Directory Purposes + +**`src/AvalonStudio.Shell.Extensibility/`:** + +- Purpose: public API surface consumed by host applications and extension assemblies. +- Contains: interfaces, MEF export attributes/metadata, ReactiveUI MVVM bases, controls, settings helpers, menu/command/toolbar services, and platform paths. +- Key files: `src/AvalonStudio.Shell.Extensibility/IoC.cs`, `src/AvalonStudio.Shell.Extensibility/IExtension.cs`, `src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs`, `src/AvalonStudio.Shell.Extensibility/MVVM/ToolViewModel.cs`. + +**`src/AvalonStudio.Shell/`:** + +- Purpose: concrete implementation of the reusable desktop shell. +- Contains: startup/composition code, MEF extension discovery, shell state, Avalonia XAML, custom controls, Dock.Avalonia adapters, themes, and bundled assets. +- Key files: `src/AvalonStudio.Shell/Shell.cs`, `src/AvalonStudio.Shell/CompositionRoot.cs`, `src/AvalonStudio.Shell/ShellViewModel.cs`, `src/AvalonStudio.Shell/ShellView.paml`. + +**`src/AvalonStudio.Shell/Docking/`:** + +- Purpose: creates and maintains the Dock.Avalonia layout used by the shell workspace. +- Contains: the default factory, custom document/tool dock types, and Dock sample views. +- Key files: `src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs`, `src/AvalonStudio.Shell/Docking/AvalonStudioDocumentDock.cs`, `src/AvalonStudio.Shell/Docking/AvalonStudioToolDock.cs`. + +**`src/AvalonStudio.Shell/Controls/`:** + +- Purpose: concrete shell controls and their matching XAML/PAML styles and code-behind. +- Contains: `MetroWindow`, `ModalDialog`, `StatusBar`, and `ToolBar` implementations. +- Key files: `src/AvalonStudio.Shell/Controls/MetroWindow.cs`, `src/AvalonStudio.Shell/Controls/MetroWindow.paml`, `src/AvalonStudio.Shell/Controls/StatusBarViewModel.cs`. + +**`src/AvalonStudio.Shell.Extensibility/Commands/`, `MainMenu/`, `Menus/`, and `Toolbars/`:** + +- Purpose: define metadata attributes and build UI models from MEF-imported feature exports. +- Contains: export attributes, metadata containers, item models, services, and settings types. +- Key files: `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs`, `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs`, `src/AvalonStudio.Shell.Extensibility/Toolbars/ToolbarService.cs`. + +**`src/AvalonStudio.Shell.Extensibility/MVVM/` and `Documents/`:** + +- Purpose: define feature-facing bases for reactive view models, dockable tools, documents, and conventional view lookup. +- Contains: `ViewModel`, `ToolViewModel`, `DocumentTabViewModel`, location enum, collection helpers, and `ViewLocator`. +- Key files: `src/AvalonStudio.Shell.Extensibility/MVVM/GenericViewModel.cs`, `src/AvalonStudio.Shell.Extensibility/MVVM/ViewLocator.cs`, `src/AvalonStudio.Shell.Extensibility/Documents/GenericDocumentTabViewModel.cs`. + +**`src/AvalonStudio.Utils/Behaviors/`:** + +- Purpose: generic declarative XAML interaction behavior collection. +- Contains: focus, command, modal-window, and pointer-wheel behaviors. +- Key files: `src/AvalonStudio.Utils/Behaviors/OpenCloseWindowBehavior.cs`, `src/AvalonStudio.Utils/Behaviors/CommandBasedBehavior.cs`. + +**`src/ShellExampleApp/`:** + +- Purpose: executable reference host demonstrating shell bootstrap and feature contribution. +- Contains: `App`, `MainWindow`, paired view/view-model classes, sample settings, sample commands, main-menu items, and toolbar exports. +- Key files: `src/ShellExampleApp/App.paml.cs`, `src/ShellExampleApp/MainWindow.xaml`, `src/ShellExampleApp/ConsoleViewModel.cs`, `src/ShellExampleApp/Commands/ThemeCommands.cs`. + +**`build/`:** + +- Purpose: Nuke build orchestration, package/version metadata, and CI helpers. +- Contains: C# build targets, shared version props, and build project files. +- Key files: `build/Build.cs`, `build/BuildParameters.cs`, `build/SharedVersion.props`, `build/_build.csproj`. + +**`Numerge/`:** + +- Purpose: separate NuGet package-merging utility included in the solution. +- Contains: merger library and console project. +- Key files: `Numerge/Numerge/NugetPackageMerger.cs`, `Numerge/Numerge.Console/Program.cs`. + +## Key File Locations + +**Entry Points:** + +- `src/ShellExampleApp/App.paml.cs`: executable `Main`, Avalonia builder, and sample application initialization. +- `src/AvalonStudio.Shell/Shell.cs`: reusable `StartShellApp` extension method for host applications. +- `src/AvalonStudio.Shell.sln`: groups the product, build, and Numerge projects. +- `build/Build.cs`: Nuke build target definition. + +**Configuration:** + +- `src/Directory.Build.props`: source-wide language and Avalonia build properties. +- `src/Versions.props`: centrally pinned package versions. +- `src/Directory.Build.targets`: source-wide MSBuild targets. +- `src/Packages.targets`: package-related build imports. +- `azure-pipelines.yml`: CI pipeline definition. +- `src/AvalonStudio.Shell/app.config`: legacy assembly binding redirects distributed with the shell project. + +**Core Logic:** + +- `src/AvalonStudio.Shell/CompositionRoot.cs`: MEF composition and external extension assembly loading. +- `src/AvalonStudio.Shell/ShellViewModel.cs`: document/perspective state, layout initialization, command bindings, and feature activation. +- `src/AvalonStudio.Shell/AvalonStudioPerspective.cs`: tool dock creation and selected-tool behavior. +- `src/AvalonStudio.Shell/DockExtensions.cs`: model-to-dockable adapter. +- `src/AvalonStudio.Shell.Extensibility/IoC.cs`: global MEF resolution facade. + +**Testing:** + +- Not detected: no dedicated test project or `*.test.*` / `*.spec.*` files are present under `src/`. + +## Naming Conventions + +**Files:** + +- Use PascalCase file names matching their primary C# type: `ShellViewModel.cs`, `CommandService.cs`, and `DefaultLayoutFactory.cs`. +- Pair Avalonia views with code-behind using the same basename: `MainWindow.xaml` with `MainWindow.xaml.cs`, `StatusBar.paml` with `StatusBar.paml.cs`. +- Use `*ViewModel.cs` for view-state classes and a matching `*View.xaml` or `*View.paml` control so `ViewLocator` can discover it: `src/ShellExampleApp/ConsoleViewModel.cs` and `src/ShellExampleApp/ConsoleView.xaml`. +- Use `Generic*.cs` for generic MVVM bases and the shorter conventional name for the non-generic convenience subclass: `src/AvalonStudio.Shell.Extensibility/Documents/GenericDocumentTabViewModel.cs` and `src/AvalonStudio.Shell.Extensibility/Documents/DocumentTabViewModel.cs`. +- Use `*Attribute.cs` for MEF metadata attributes and `*Metadata.cs` for their metadata objects: `src/AvalonStudio.Shell.Extensibility/Commands/ExportCommandDefinitionAttribute.cs`, `src/AvalonStudio.Shell.Extensibility/Commands/CommandDefinitionMetadata.cs`. + +**Directories:** + +- Group reusable contracts by domain under `src/AvalonStudio.Shell.Extensibility/` rather than by caller: `Commands/`, `Menus/`, `Toolbars/`, `Settings/`, `Shell/`, `Theme/`, and `Platforms/`. +- Group shell implementation by UI/runtime concern under `src/AvalonStudio.Shell/`: `Controls/`, `Docking/`, `Themes/`, `Styles/`, `Converters/`, and `Behaviors/`. +- Put application-level UI contributions in `src/ShellExampleApp/Commands/`, `src/ShellExampleApp/MainMenu/`, and `src/ShellExampleApp/Toolbars/`. + +## Where to Add New Code + +**New Plugin-Facing Feature Contract:** + +- Public interface, base class, export attribute, or metadata: add it to the closest domain folder in `src/AvalonStudio.Shell.Extensibility/`. +- Shell-specific realization: add it to the closest runtime folder in `src/AvalonStudio.Shell/`. +- Do not make the extensibility project reference the shell project; maintain the direction declared in `src/AvalonStudio.Shell.sln` and the `.csproj` project references. + +**New Document Feature:** + +- Primary code: subclass `DocumentTabViewModel` in the owning application/extension feature directory, as in `src/ShellExampleApp/DocumentViewModel.cs`. +- View: create a matching `*View.xaml` plus `*View.xaml.cs` in the same namespace/directory, as in `src/ShellExampleApp/DocumentView.xaml`. +- Open/select: call `IShell.AddOrSelectDocument` through `src/AvalonStudio.Shell.Extensibility/Shell/IShellExtensions.cs`. + +**New Tool Pane:** + +- Primary code: subclass `ToolViewModel`, set `DefaultLocation`, implement `IActivatableExtension` when it should appear at startup, and export it with `[ExportToolControl]` in the owning application/extension project. +- Example: `src/ShellExampleApp/SolutionExplorerViewModel.cs`. +- View: co-locate a parameterless matching `SolutionExplorerView.xaml` / `.xaml.cs` type for conventional resolution. + +**New Command/Menu/Toolbar Contribution:** + +- Command definition: add a MEF-composed provider under the host/extension's `Commands/` directory and use `ExportCommandDefinitionAttribute`; see `src/ShellExampleApp/Commands/ThemeCommands.cs`. +- Main menu item: add an attributed MEF provider under `MainMenu/`; see `src/ShellExampleApp/MainMenu/ThemeMainMenuItem.cs`. +- Toolbar: add its declarative export under `Toolbars/`; see `src/ShellExampleApp/Toolbars/StandardToolbar.cs`. + +**New Shell Control or Shared Behavior:** + +- Shell-specific custom control: use `src/AvalonStudio.Shell/Controls/` with paired `.paml` and `.paml.cs` files when it has a template/style. +- Contract-layer reusable control: use `src/AvalonStudio.Shell.Extensibility/Controls/`. +- Framework-agnostic shell behavior: use `src/AvalonStudio.Utils/Behaviors/`. + +**Utilities:** + +- Shell-only helper: place next to its shell domain in `src/AvalonStudio.Shell/` or the existing `Docking/` / `Converters/` folders. +- Plugin-safe helper: place under `src/AvalonStudio.Shell.Extensibility/Utils/` only when it belongs to the public extensibility API. +- Generic Avalonia interaction helper: place under `src/AvalonStudio.Utils/Behaviors/`. + +## Special Directories + +**`src/AvalonStudio.Shell/Assets/`:** + +- Purpose: packaged fonts and image resources consumed by shell XAML and themes. +- Generated: No. +- Committed: Yes. + +**`src/AvalonStudio.Shell/Themes/`, `Styles/`, and `Icons/`:** + +- Purpose: Avalonia resource dictionaries included by the sample app's `App.paml`. +- Generated: No. +- Committed: Yes. + +**`src/**/bin/` and `src/**/obj/`:** + +- Purpose: MSBuild output and intermediates. +- Generated: Yes. +- Committed: No; exclude these paths from source changes and exploration. + +**`src/.vs/`:** + +- Purpose: Visual Studio workspace caches and user-local indexes. +- Generated: Yes. +- Committed: No. + +**`.nuke/`:** + +- Purpose: Nuke build parameter schema and generated build metadata. +- Generated: Mixed; treat files as build tooling metadata. +- Committed: Yes. + +**`Numerge/`:** + +- Purpose: separately versioned package-merge code nested with its own repository metadata. +- Generated: No. +- Committed: Yes at the superproject level; preserve its internal `.git` metadata and do not treat it as normal `src/` product code. + +--- + +*Structure analysis: 2026-08-28* diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md new file mode 100644 index 0000000..ccc160e --- /dev/null +++ b/.planning/codebase/TESTING.md @@ -0,0 +1,117 @@ +# Testing Patterns + +**Analysis Date:** 2026-08-28 + +## Test Framework + +**Runner:** +- Not detected. No test project, test SDK package, runner configuration, or `*.test.*` / `*.spec.*` source is present in the repository. +- The production solution at `src/AvalonStudio.Shell.sln` contains `AvalonStudio.Shell.Extensibility`, `AvalonStudio.Shell`, `AvalonStudio.Utils`, `ShellExampleApp`, `_build`, and Numerge; it contains no test project. `Numerge/Numerge.sln` likewise contains only the Numerge library and console project. +- Config: Not applicable; no `xunit.runner.json`, `nunit`, MSTest, NUnit, xUnit, or Playwright configuration file is detected. + +**Assertion Library:** +- Not detected. No assertions or test-framework imports are present in tracked C# source. + +**Run Commands:** +```powershell +# No test command is configured. +# `azure-pipelines.yml` invokes `dotnet run --project build/_build.csproj --configuration Release -- --target CiAzureWindows`. +# That Nuke target builds and packs; `build/Build.cs` defines no test target or DotNetTest invocation. +``` + +## Test File Organization + +**Location:** +- Not applicable. There is no `tests/` directory despite `TestsDirectory => RootDirectory / "tests"` being declared in `build/Build.cs`. + +**Naming:** +- Not established. Do not infer a `*.Tests` project naming convention from this repository. + +**Structure:** +```text +AvalonStudio.Shell/ +├── src/ # Production projects only +├── Numerge/ # Package-merger library and console projects +└── build/ # Nuke build project; no test target +``` + +## Test Structure + +**Suite Organization:** +```csharp +// Not applicable: no test classes, fixtures, or suites are present. +// Production verification currently occurs by building the solutions through `build/Build.cs`. +``` + +**Patterns:** +- Setup pattern: Not detected. +- Teardown pattern: Not detected. +- Assertion pattern: Not detected. +- The codebase relies on framework integration points that need isolation if tests are introduced: MEF composition in `src/AvalonStudio.Shell/CompositionRoot.cs`, the static IoC host in `src/AvalonStudio.Shell.Extensibility/IoC.cs`, Avalonia UI-thread dispatch in `src/AvalonStudio.Shell.Extensibility/MVVM/BindableCollection.cs`, and ReactiveUI notifications in `src/AvalonStudio.Shell/Controls/StatusBarViewModel.cs`. + +## Mocking + +**Framework:** +- Not detected. No mocking library package or mock calls are present. + +**Patterns:** +```csharp +// Not applicable: no mocks, fakes, or stubs are defined in this repository. +``` + +**What to Mock:** +- No repository-established rule exists. If a test suite is added, isolate the MEF/extension boundary consumed by `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs` and `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs` rather than requiring installed extensions. +- For merge logic, `Numerge/Numerge/NugetPackageMerger.cs` already accepts `INumergeLogger`; supply a test logger through that interface instead of intercepting console output. + +**What NOT to Mock:** +- No repository-established rule exists. Preserve actual pure collection and model behavior when testing `src/AvalonStudio.Shell.Extensibility/Menus/MenuPath.cs`, `src/AvalonStudio.Shell.Extensibility/GlobalSettings/Mapper.cs`, and similar deterministic classes. + +## Fixtures and Factories + +**Test Data:** +```csharp +// Not applicable: no fixtures, builders, factories, or test-data directories exist. +``` + +**Location:** +- Not applicable. No fixture location is established. + +## Coverage + +**Requirements:** No coverage target or coverage collection configuration is detected in `azure-pipelines.yml`, `build/Build.cs`, the solution files, or project files. + +**View Coverage:** +```powershell +# Not configured. No coverage collector or reporting command exists. +``` + +## Test Types + +**Unit Tests:** +- Not used. No unit-test sources or unit-test project are detected. + +**Integration Tests:** +- Not used. The CI workflow in `azure-pipelines.yml` invokes the `CiAzureWindows` target in `build/Build.cs`, which compiles and packages the project but does not execute a test command. + +**E2E Tests:** +- Not used. No UI automation framework or E2E configuration is detected for the Avalonia example app in `src/ShellExampleApp/`. + +## Common Patterns + +**Async Testing:** +```csharp +// Not applicable: no async test method exists. +// Production async UI work appears in `src/AvalonStudio.Shell.Extensibility/Dialogs/ModalDialogViewModelBase.cs` +// and UI-dispatched collection operations in `src/AvalonStudio.Shell.Extensibility/MVVM/BindableCollection.cs`. +``` + +**Error Testing:** +```csharp +// Not applicable: no exception or failure-result assertions exist. +// `Numerge/Numerge/NugetPackageMerger.cs` returns false for MergeAbortedException; +// `src/AvalonStudio.Shell/CompositionRoot.cs` logs and continues after a component load failure. +``` + +--- + +*Testing analysis: 2026-08-28* From ba1904bdb79991efe9d9de89d824c771691fea44 Mon Sep 17 00:00:00 2001 From: jcguo Date: Sat, 29 Aug 2026 09:33:15 +0800 Subject: [PATCH 8/8] fix: enable compiled Avalonia bindings --- .../Controls/ModalDialog.paml | 6 +- .../Controls/StatusBar.paml | 4 +- src/AvalonStudio.Shell/Controls/ToolBar.paml | 4 +- src/AvalonStudio.Shell/MenuShellView.paml | 1 + src/AvalonStudio.Shell/ShellView.paml | 8 +- .../Behaviors/OpenCloseWindowBehavior.cs | 101 ++++++++++++++---- src/Directory.Build.props | 1 - src/ShellExampleApp/AboutDialogView.xaml | 4 +- src/ShellExampleApp/App.paml | 17 --- src/ShellExampleApp/MainWindow.xaml | 9 +- src/ShellExampleApp/MainWindowViewModel.cs | 4 +- 11 files changed, 102 insertions(+), 57 deletions(-) diff --git a/src/AvalonStudio.Shell/Controls/ModalDialog.paml b/src/AvalonStudio.Shell/Controls/ModalDialog.paml index 8d5384d..b98e9a4 100644 --- a/src/AvalonStudio.Shell/Controls/ModalDialog.paml +++ b/src/AvalonStudio.Shell/Controls/ModalDialog.paml @@ -2,7 +2,9 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:AvalonStudio.Shell.Controls;assembly=AvalonStudio.Shell" - xmlns:cont="clr-namespace:AvalonStudio.Controls;assembly=AvalonStudio.Shell.Extensibility" + xmlns:cont="clr-namespace:AvalonStudio.Controls;assembly=AvalonStudio.Shell.Extensibility" + xmlns:dialogs="clr-namespace:AvalonStudio.Extensibility.Dialogs;assembly=AvalonStudio.Shell.Extensibility" + x:DataType="dialogs:ModalDialogViewModelBase" Background="{DynamicResource ThemeBackgroundBrush}" Icon="avares://AvalonStudio.Shell/Assets/logo-256.png" Title="{Binding Title}" @@ -17,4 +19,4 @@ - \ No newline at end of file + diff --git a/src/AvalonStudio.Shell/Controls/StatusBar.paml b/src/AvalonStudio.Shell/Controls/StatusBar.paml index 5291bb0..05a0558 100644 --- a/src/AvalonStudio.Shell/Controls/StatusBar.paml +++ b/src/AvalonStudio.Shell/Controls/StatusBar.paml @@ -1,6 +1,8 @@  @@ -10,4 +12,4 @@ - \ No newline at end of file + diff --git a/src/AvalonStudio.Shell/Controls/ToolBar.paml b/src/AvalonStudio.Shell/Controls/ToolBar.paml index 54c0bf7..315b51b 100644 --- a/src/AvalonStudio.Shell/Controls/ToolBar.paml +++ b/src/AvalonStudio.Shell/Controls/ToolBar.paml @@ -1,7 +1,9 @@  + xmlns:mod="clr-namespace:AvalonStudio.Menus.Models;assembly=AvalonStudio.Shell.Extensibility" + xmlns:vm="clr-namespace:AvalonStudio.Toolbars.ViewModels;assembly=AvalonStudio.Shell.Extensibility" + x:DataType="vm:ToolbarViewModel"> - - - - + diff --git a/src/AvalonStudio.Utils/Behaviors/OpenCloseWindowBehavior.cs b/src/AvalonStudio.Utils/Behaviors/OpenCloseWindowBehavior.cs index 9e695ab..b6f5f85 100644 --- a/src/AvalonStudio.Utils/Behaviors/OpenCloseWindowBehavior.cs +++ b/src/AvalonStudio.Utils/Behaviors/OpenCloseWindowBehavior.cs @@ -4,12 +4,16 @@ using Avalonia.VisualTree; using Avalonia.Xaml.Interactivity; using System; +using System.Collections.Generic; using System.Reactive.Disposables; +using System.Runtime.CompilerServices; namespace AvalonStudio.Utils.Behaviors { public class OpenCloseWindowBehavior : Behavior { + private static readonly ConditionalWeakTable OpenWindows = new(); + private CompositeDisposable _disposables; private Window _currentWindow; @@ -20,37 +24,62 @@ protected override void OnAttached() _disposables = new CompositeDisposable { this.GetObservable(IsOpenProperty).Subscribe(open => { - if(open && _currentWindow == null) + if (open) { - if(WindowType != null) - { - _currentWindow = Activator.CreateInstance(WindowType) as Window; - } - else - { - _currentWindow = Activator.CreateInstance(); - } - - if (DataContext == null) - { - _currentWindow.DataContext = AssociatedObject.DataContext; - } - else - { - _currentWindow.DataContext = DataContext; - } - - var mainWindow = TopLevel.GetTopLevel(AssociatedObject) as Window; - _currentWindow.ShowDialog(mainWindow); + OpenWindow(); } else { - _currentWindow?.Close(); - _currentWindow = null; + CloseWindow(); } })}; } + private void OpenWindow() + { + if (_currentWindow?.IsVisible == true) + { + return; + } + + _currentWindow = null; + + var dataContext = DataContext ?? AssociatedObject.DataContext; + var windowType = WindowType ?? typeof(Window); + WindowRegistry registry = null; + + if (dataContext != null) + { + registry = OpenWindows.GetValue(dataContext, _ => new WindowRegistry()); + + if (registry.TryGet(windowType, out var existingWindow) && existingWindow.IsVisible) + { + _currentWindow = existingWindow; + existingWindow.Activate(); + return; + } + } + + _currentWindow = Activator.CreateInstance(windowType) as Window; + _currentWindow.DataContext = dataContext; + + if (registry != null) + { + var window = _currentWindow; + registry.Set(windowType, window); + window.Closed += (_, _) => registry.Remove(windowType, window); + } + + var mainWindow = TopLevel.GetTopLevel(AssociatedObject) as Window; + _currentWindow.ShowDialog(mainWindow); + } + + private void CloseWindow() + { + _currentWindow?.Close(); + _currentWindow = null; + } + protected override void OnDetaching() { _disposables.Dispose(); @@ -58,6 +87,32 @@ protected override void OnDetaching() base.OnDetaching(); } + private sealed class WindowRegistry + { + private readonly Dictionary> _windows = new(); + + public bool TryGet(Type windowType, out Window window) + { + window = null; + + return _windows.TryGetValue(windowType, out var reference) + && reference.TryGetTarget(out window); + } + + public void Set(Type windowType, Window window) + { + _windows[windowType] = new WeakReference(window); + } + + public void Remove(Type windowType, Window window) + { + if (TryGet(windowType, out var currentWindow) && ReferenceEquals(currentWindow, window)) + { + _windows.Remove(windowType); + } + } + } + public static readonly StyledProperty WindowTypeProperty = AvaloniaProperty.Register(nameof(WindowType), defaultBindingMode: BindingMode.TwoWay); diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 0ce61e5..6ee50b1 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,6 @@ Latest true - false diff --git a/src/ShellExampleApp/AboutDialogView.xaml b/src/ShellExampleApp/AboutDialogView.xaml index 089523a..8f591dd 100644 --- a/src/ShellExampleApp/AboutDialogView.xaml +++ b/src/ShellExampleApp/AboutDialogView.xaml @@ -1,6 +1,8 @@  diff --git a/src/ShellExampleApp/App.paml b/src/ShellExampleApp/App.paml index 34aa774..3896b68 100644 --- a/src/ShellExampleApp/App.paml +++ b/src/ShellExampleApp/App.paml @@ -7,7 +7,6 @@ xmlns:Documents="clr-namespace:AvaloniaDemo.ViewModels.Documents;assembly=AvalonStudio" xmlns:Tools="clr-namespace:AvaloniaDemo.ViewModels.Tools;assembly=AvalonStudio" xmlns:cont="clr-namespace:AvalonStudio.Controls;assembly=AvalonStudio.Shell.Extensibility" - xmlns:Views="clr-namespace:AvaloniaDemo.ViewModels.Views;assembly=AvalonStudio.Shell" x:Class="ShellExampleApp.App"> @@ -20,22 +19,6 @@ - - - - - - - -