From 14c585ec7d59c16e8ff3006315c6c8bcdbe77125 Mon Sep 17 00:00:00 2001 From: Curtis Wensley Date: Sat, 17 Oct 2026 09:32:34 -0700 Subject: [PATCH 1/2] Fix VS threading analyzer warnings Make sure VS settings and editor calls happen on the UI thread, and switch to it explicitly before drawing a preview rather than relying on the caller. Co-Authored-By: Claude Opus 5.5 --- .../Editor/EtoPreviewPane.cs | 16 +++++++++++++--- .../Editor/PreviewHostClient.cs | 3 +++ .../Editor/PreviewPlatforms.cs | 14 ++++++++++++-- .../Wizards/WindowTitleBar.cs | 13 ++++++++++++- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Editor/EtoPreviewPane.cs b/src/Eto.DevExtension.VisualStudio.Windows/Editor/EtoPreviewPane.cs index 0c6c1f4..f265729 100644 --- a/src/Eto.DevExtension.VisualStudio.Windows/Editor/EtoPreviewPane.cs +++ b/src/Eto.DevExtension.VisualStudio.Windows/Editor/EtoPreviewPane.cs @@ -125,6 +125,7 @@ void RegisterIndependentView(bool subscribe) public EtoPreviewPane(EtoAddinPackage package, string fileName, IVsTextLines textBuffer, string projectKey, CodeEditorHost codeEditor) : base(package) { + ThreadHelper.ThrowIfNotOnUIThread(); this.editor = codeEditor; this.package = package; this.textBuffer = textBuffer; @@ -138,6 +139,7 @@ public EtoPreviewPane(EtoAddinPackage package, string fileName, IVsTextLines tex host.ProjectChanged += PreviewHost_ProjectChanged; var designHost = new RemoteDesignPanel(async request => { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); var snapshot = documentBuffer?.CurrentSnapshot; var result = await host.RenderAsync(request, await ProjectAssemblyPaths.GetAsync(fileName)); if (snapshot != null && !disposed) @@ -163,7 +165,11 @@ public EtoPreviewPane(EtoAddinPackage package, string fileName, IVsTextLines tex }; preview = previewSplitter.Preview; PreviewLayoutSettings.Load(previewSplitter); - previewSplitter.LayoutChanged += (sender, e) => PreviewLayoutSettings.Save(previewSplitter); + previewSplitter.LayoutChanged += (sender, e) => + { + ThreadHelper.ThrowIfNotOnUIThread(); + PreviewLayoutSettings.Save(previewSplitter); + }; platformDropDown = new DropDown { ToolTip = "Platform to draw the preview with" }; platformDropDown.SelectedKeyChanged += PlatformDropDown_SelectedKeyChanged; @@ -307,7 +313,7 @@ protected override void Initialize() // the document may not be registered yet while the frame is being created if (!LockDocument()) - ThreadHelper.JoinableTaskFactory.StartOnIdle(() => LockDocument()); + _ = ThreadHelper.JoinableTaskFactory.StartOnIdle(() => LockDocument()); preview.Update(); } @@ -581,7 +587,11 @@ public override int LoadUIState(Stream stateStream) public int GetViewClassID(out Guid pclsidView) => editor.codeWindow.GetViewClassID(out pclsidView); public int SetBaseEditorCaption(string[] pszBaseEditorCaption) => editor.codeWindow.SetBaseEditorCaption(pszBaseEditorCaption); public int GetEditorCaption(READONLYSTATUS dwReadOnly, out string pbstrEditorCaption) => editor.codeWindow.GetEditorCaption(dwReadOnly, out pbstrEditorCaption); - public int Close() => editor.Close(); + public int Close() + { + ThreadHelper.ThrowIfNotOnUIThread(); + return editor.Close(); + } public int GetLastActiveView(out IVsTextView ppView) => editor.codeWindow.GetLastActiveView(out ppView); int IVsCodeWindowEx.Initialize(uint grfCodeWindowBehaviorFlags, VSUSERCONTEXTATTRIBUTEUSAGE usageAuxUserContext, string szNameAuxUserContext, string szValueAuxUserContext, uint InitViewFlags, INITVIEW[] pInitView) => ((IVsCodeWindowEx)editor.codeWindow).Initialize(grfCodeWindowBehaviorFlags, usageAuxUserContext, szNameAuxUserContext, szValueAuxUserContext, InitViewFlags, pInitView); diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewHostClient.cs b/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewHostClient.cs index 982f158..bf852ba 100644 --- a/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewHostClient.cs +++ b/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewHostClient.cs @@ -1,5 +1,6 @@ using Eto.Designer; using Eto.Drawing; +using Microsoft.VisualStudio.Shell; using Newtonsoft.Json.Linq; using StreamJsonRpc; using System; @@ -71,6 +72,8 @@ public async Task RenderAsync(PreviewRenderRequest request, await requestLock.WaitAsync(); try { + // the platform and theme choices are read from VS settings + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); // a second try covers a host that exited after a rebuild, or that serves an older build var wanted = await PreviewPlatforms.ResolveAsync(); var theme = PreviewPlatforms.ThemeChoice; diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewPlatforms.cs b/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewPlatforms.cs index 96a4dd4..0e1bedc 100644 --- a/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewPlatforms.cs +++ b/src/Eto.DevExtension.VisualStudio.Windows/Editor/PreviewPlatforms.cs @@ -86,9 +86,14 @@ public static List GetAvailable() /// A platform id or , remembered for each solution. Use on the UI thread. public static string Choice { - get => GetChoice(CollectionPath, Auto, unsavedChoice); + get + { + ThreadHelper.ThrowIfNotOnUIThread(); + return GetChoice(CollectionPath, Auto, unsavedChoice); + } set { + ThreadHelper.ThrowIfNotOnUIThread(); if (SetChoice(CollectionPath, value ?? Auto, Auto, ref unsavedChoice)) ChoiceChanged?.Invoke(null, EventArgs.Empty); } @@ -97,9 +102,14 @@ public static string Choice /// A theme name from the host, or , remembered for each solution. Use on the UI thread. public static string ThemeChoice { - get => GetChoice(ThemeCollectionPath, DefaultTheme, unsavedTheme); + get + { + ThreadHelper.ThrowIfNotOnUIThread(); + return GetChoice(ThemeCollectionPath, DefaultTheme, unsavedTheme); + } set { + ThreadHelper.ThrowIfNotOnUIThread(); if (SetChoice(ThemeCollectionPath, value ?? DefaultTheme, DefaultTheme, ref unsavedTheme)) ThemeChoiceChanged?.Invoke(null, EventArgs.Empty); } diff --git a/src/Eto.DevExtension.VisualStudio.Windows/Wizards/WindowTitleBar.cs b/src/Eto.DevExtension.VisualStudio.Windows/Wizards/WindowTitleBar.cs index a9f169e..6c11cac 100644 --- a/src/Eto.DevExtension.VisualStudio.Windows/Wizards/WindowTitleBar.cs +++ b/src/Eto.DevExtension.VisualStudio.Windows/Wizards/WindowTitleBar.cs @@ -1,4 +1,6 @@ using Microsoft.VisualStudio.PlatformUI; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Threading; using System; using System.Runtime.InteropServices; using System.Windows; @@ -23,7 +25,16 @@ static class WindowTitleBar public static void Attach(Window window) { void Apply() => ApplyTheme(window); - void ThemeChanged(ThemeChangedEventArgs e) => window.Dispatcher.BeginInvoke((Action)Apply); + void ThemeChanged(ThemeChangedEventArgs e) + { + // may be raised off the UI thread + var factory = EtoAddinPackage.Instance?.JoinableTaskFactory ?? ThreadHelper.JoinableTaskFactory; + factory.RunAsync(async () => + { + await factory.SwitchToMainThreadAsync(); + Apply(); + }).Task.Forget(); + } if (new WindowInteropHelper(window).Handle != IntPtr.Zero) Apply(); From 47f4a65420e5ef6e5c11dc8401d2b26677524f4f Mon Sep 17 00:00:00 2001 From: Curtis Wensley Date: Sat, 17 Oct 2026 09:32:34 -0700 Subject: [PATCH 2/2] Remove duplicate Roslyn package reference Co-Authored-By: Claude Opus 5.5 --- .../Eto.DevExtension.LanguageServer.csproj | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Eto.DevExtension.LanguageServer/Eto.DevExtension.LanguageServer.csproj b/src/Eto.DevExtension.LanguageServer/Eto.DevExtension.LanguageServer.csproj index 12b66aa..fe49ca9 100644 --- a/src/Eto.DevExtension.LanguageServer/Eto.DevExtension.LanguageServer.csproj +++ b/src/Eto.DevExtension.LanguageServer/Eto.DevExtension.LanguageServer.csproj @@ -24,11 +24,6 @@ - - - - -