From 8cc959899178a14142bb7150fe10fdcc62faeada Mon Sep 17 00:00:00 2001 From: Nice3point Date: Fri, 30 Oct 2026 22:12:18 +0300 Subject: [PATCH 1/5] [Context] Resume the rendering of the WPF windows of the Revit thread after a file upgrade --- CHANGELOG.md | 13 ++ README.md | 15 ++ .../Internal/NativeMethods.cs | 29 +++ .../Nice3point.Revit.Toolkit/RevitContext.cs | 86 ++++++++- .../ColorWindow.cs | 174 ++++++++++++++++++ .../RevitContextTests.cs | 42 +++++ 6 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 source/Nice3point.Revit.Toolkit/Internal/NativeMethods.cs create mode 100644 tests/Nice3point.Revit.Toolkit.Tests/ColorWindow.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0239b86..03f9f57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# 2027.0.4 + +### Context + +- New `RevitContext.BeginRenderingResumptionScope()` method resumes the rendering of the WPF windows of the Revit thread after Revit upgrades a family, a template, or a document of an earlier version. Previously, the progress dialog of the upgrade dropped the frame message of WPF, and the ribbon, the panes, and the windows of an add-in stopped drawing until a WPF window changed its size. + +```c# +using (RevitContext.BeginRenderingResumptionScope()) +{ + document.LoadFamily(fileName, out var family); +} +``` + # 2027.0.3 ### External Events diff --git a/README.md b/README.md index 79241ce..a88ef2f 100644 --- a/README.md +++ b/README.md @@ -834,6 +834,21 @@ using (RevitContext.BeginDialogSuppressionScope(args => } ``` +**Resuming rendering after a file upgrade** + +Revit upgrades a family, a template, or a document of an earlier version when `Document.LoadFamily`, `Application.NewFamilyDocument`, or `Application.OpenDocumentFile` opens it. +While it upgrades, its progress dialog drops the message WPF waits for before it draws the next frame, and the ribbon, the panes, and the windows of an add-in stop drawing. +Wrap the calls that can upgrade a file in `BeginRenderingResumptionScope()` on the Revit thread: + +```C# +using (RevitContext.BeginRenderingResumptionScope()) +{ + document.LoadFamily(fileName, out var family); +} +``` + +The WPF windows of the thread draw again when the last active scope is disposed. + ### Options The Toolkit provides implementation of various Revit interfaces. diff --git a/source/Nice3point.Revit.Toolkit/Internal/NativeMethods.cs b/source/Nice3point.Revit.Toolkit/Internal/NativeMethods.cs new file mode 100644 index 0000000..3824a4a --- /dev/null +++ b/source/Nice3point.Revit.Toolkit/Internal/NativeMethods.cs @@ -0,0 +1,29 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace Nice3point.Revit.Toolkit.Internal; + +/// +/// Provides the Win32 functions the toolkit calls on the windows of a thread. +/// +internal static class NativeMethods +{ + internal delegate bool EnumThreadWindowsCallback(IntPtr window, IntPtr parameter); + + [DllImport("kernel32.dll")] + internal static extern uint GetCurrentThreadId(); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool EnumThreadWindows(uint threadId, EnumThreadWindowsCallback callback, IntPtr parameter); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern int GetWindowText(IntPtr window, StringBuilder text, int maxCount); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern uint RegisterWindowMessage(string message); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool PostMessage(IntPtr window, uint message, IntPtr wParam, IntPtr lParam); +} diff --git a/source/Nice3point.Revit.Toolkit/RevitContext.cs b/source/Nice3point.Revit.Toolkit/RevitContext.cs index 3f4d160..e10be25 100644 --- a/source/Nice3point.Revit.Toolkit/RevitContext.cs +++ b/source/Nice3point.Revit.Toolkit/RevitContext.cs @@ -1,11 +1,10 @@ using System.Reflection; using System.Runtime.InteropServices; +using System.Text; using System.Windows; using Autodesk.Revit.UI; using Autodesk.Revit.UI.Events; -#if REVIT2027_OR_GREATER using Nice3point.Revit.Toolkit.Internal; -#endif namespace Nice3point.Revit.Toolkit; @@ -27,6 +26,13 @@ public class RevitContext : RevitApiContext private static int? _suppressDialogCode; private static Action? _suppressDialogHandler; + private const string RenderNotificationWindowName = "MediaContextNotificationWindow"; + private static readonly uint RenderNotificationMessage = NativeMethods.RegisterWindowMessage("MilChannelNotify"); + private static readonly NativeMethods.EnumThreadWindowsCallback PostRenderNotificationCallback = PostRenderNotification; + private static readonly Lock RenderingLock = new(); + private static int _renderingScopeCount; + private static uint _renderingThreadId; + static RevitContext() { #if REVIT2027_OR_GREATER @@ -353,6 +359,48 @@ public static IDisposable BeginDialogSuppressionScope(Action + /// Begins a scope that resumes the rendering of the WPF windows of the current thread after Revit upgrades a file. + /// The rendering resumes when the outermost scope is disposed. + /// + /// A disposable scope. Call Dispose or use 'using' statement to resume the rendering. + /// + /// + /// Revit upgrades a family, a template, or a document of an earlier version when + /// , , + /// or opens it. + /// While it upgrades, the progress dialog of Revit drops the message the WPF render thread posts after each frame, + /// and WPF presents no further frame of the thread until that message arrives: the ribbon, the panes, and the windows of an add-in stop drawing. + /// + /// + /// Wrap the calls that can upgrade a file in the scope. + /// When the outermost scope is disposed, the scope posts the message again to the thread that began it, and the WPF windows of that thread draw again. + /// Begin the scope on the thread of the windows, the Revit main thread. + /// + /// This method is thread-safe. + /// + /// + /// + /// using (RevitContext.BeginRenderingResumptionScope()) + /// { + /// document.LoadFamily(fileName, out var family); + /// } + /// // The WPF windows draw again + /// + /// + public static IDisposable BeginRenderingResumptionScope() + { + lock (RenderingLock) + { + if (_renderingScopeCount++ == 0) + { + _renderingThreadId = NativeMethods.GetCurrentThreadId(); + } + } + + return new RenderingResumptionScope(); + } + internal static IDisposable BeginApiContextScope() { return new ApiContextScope(IncrementConstructorPointer, IncrementDestructorPointer); @@ -378,6 +426,19 @@ private static void ResolveDialogBox(object? sender, DialogBoxShowingEventArgs a handler?.Invoke(args); } + private static bool PostRenderNotification(IntPtr window, IntPtr parameter) + { + var name = new StringBuilder(RenderNotificationWindowName.Length + 2); + NativeMethods.GetWindowText(window, name, name.Capacity); + if (name.ToString() != RenderNotificationWindowName) + { + return true; + } + + NativeMethods.PostMessage(window, RenderNotificationMessage, IntPtr.Zero, IntPtr.Zero); + return false; + } + private sealed class ApiContextScope : IDisposable { private readonly IntPtr _deconstructorPointer; @@ -431,6 +492,27 @@ public void Dispose() } } + private sealed class RenderingResumptionScope : IDisposable + { + private int _disposed; + + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, value: 1) != 0) + { + return; + } + + lock (RenderingLock) + { + if (--_renderingScopeCount == 0) + { + NativeMethods.EnumThreadWindows(_renderingThreadId, PostRenderNotificationCallback, IntPtr.Zero); + } + } + } + } + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr IncrementCtor(IntPtr self); diff --git a/tests/Nice3point.Revit.Toolkit.Tests/ColorWindow.cs b/tests/Nice3point.Revit.Toolkit.Tests/ColorWindow.cs new file mode 100644 index 0000000..206fd95 --- /dev/null +++ b/tests/Nice3point.Revit.Toolkit.Tests/ColorWindow.cs @@ -0,0 +1,174 @@ +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.Animation; +using Color = System.Windows.Media.Color; +using Rectangle = System.Windows.Shapes.Rectangle; + +namespace Nice3point.Revit.Toolkit.Tests; + +/// +/// A WPF window of the test thread filled with one color, outside the taskbar, with one column of pixels on the screen. +/// Windows composes no frame for a window entirely outside the screen. +/// +/// +/// The color the window shows is the color of the frame Windows composes for it, the last frame WPF presented. +/// A square in its corner pulses on every frame, as the windows of Revit draw while Revit works: Revit drops the frame message only while WPF draws. +/// +internal sealed class ColorWindow : IDisposable +{ + private const double WindowSize = 40; + private const double VisibleWidth = 1; + private const double PulseSize = 4; + private const double PulseMinimumOpacity = 0.2; + private const uint RenderFullContent = 2; + private const int RedToGreenShift = 8; + private const int RedToBlueShift = 16; + + private static readonly TimeSpan DrawTimeout = TimeSpan.FromSeconds(2); + private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(50); + private static readonly TimeSpan PulseDuration = TimeSpan.FromMilliseconds(500); + + private readonly Window _window; + private readonly IntPtr _handle; + + private ColorWindow(Color color) + { + _window = new Window + { + Width = WindowSize, + Height = WindowSize, + Left = SystemParameters.VirtualScreenLeft + VisibleWidth - WindowSize, + Top = SystemParameters.VirtualScreenTop, + WindowStyle = WindowStyle.None, + ResizeMode = ResizeMode.NoResize, + ShowInTaskbar = false, + ShowActivated = false, + Background = new SolidColorBrush(color) + }; + + var pulse = new Rectangle + { + Width = PulseSize, + Height = PulseSize, + Fill = Brushes.Black, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top + }; + + pulse.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(PulseMinimumOpacity, PulseDuration) + { + AutoReverse = true, + RepeatBehavior = RepeatBehavior.Forever + }); + + _window.Content = pulse; + _window.Show(); + _handle = new WindowInteropHelper(_window).Handle; + } + + /// + /// Shows the window and waits until it shows the color. + /// + public static async Task ShowAsync(Color color, CancellationToken cancellationToken) + { + var window = new ColorWindow(color); + var drawnColor = await window.WaitForDrawnColorAsync(color, cancellationToken); + if (drawnColor != color) + { + window.Dispose(); + throw new InvalidOperationException($"The window shows {drawnColor} in place of {color}."); + } + + return window; + } + + public void Fill(Color color) + { + _window.Background = new SolidColorBrush(color); + } + + /// + /// Returns the color the window shows once it is the expected color, or the color it shows when the time to draw a frame runs out. + /// + public async Task WaitForDrawnColorAsync(Color expectedColor, CancellationToken cancellationToken) + { + var deadline = DateTime.UtcNow + DrawTimeout; + var drawnColor = ReadDrawnColor(); + while (drawnColor != expectedColor && DateTime.UtcNow < deadline) + { + await Task.Delay(PollInterval, cancellationToken); + drawnColor = ReadDrawnColor(); + } + + return drawnColor; + } + + public void Dispose() + { + _window.Close(); + } + + private Color ReadDrawnColor() + { + GetClientRect(_handle, out var bounds); + var windowContext = GetDC(_handle); + var memoryContext = CreateCompatibleDC(windowContext); + var bitmap = CreateCompatibleBitmap(windowContext, bounds.Right, bounds.Bottom); + var previousObject = SelectObject(memoryContext, bitmap); + try + { + PrintWindow(_handle, memoryContext, RenderFullContent); + var pixel = GetPixel(memoryContext, bounds.Right / 2, bounds.Bottom / 2); + + return Color.FromRgb((byte)pixel, (byte)(pixel >> RedToGreenShift), (byte)(pixel >> RedToBlueShift)); + } + finally + { + SelectObject(memoryContext, previousObject); + DeleteObject(bitmap); + DeleteDC(memoryContext); + ReleaseDC(_handle, windowContext); + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct ClientBounds + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [DllImport("user32.dll")] + private static extern bool GetClientRect(IntPtr window, out ClientBounds bounds); + + [DllImport("user32.dll")] + private static extern IntPtr GetDC(IntPtr window); + + [DllImport("user32.dll")] + private static extern int ReleaseDC(IntPtr window, IntPtr context); + + [DllImport("user32.dll")] + private static extern bool PrintWindow(IntPtr window, IntPtr context, uint flags); + + [DllImport("gdi32.dll")] + private static extern IntPtr CreateCompatibleDC(IntPtr context); + + [DllImport("gdi32.dll")] + private static extern IntPtr CreateCompatibleBitmap(IntPtr context, int width, int height); + + [DllImport("gdi32.dll")] + private static extern IntPtr SelectObject(IntPtr context, IntPtr graphicsObject); + + [DllImport("gdi32.dll")] + private static extern bool DeleteObject(IntPtr graphicsObject); + + [DllImport("gdi32.dll")] + private static extern bool DeleteDC(IntPtr context); + + [DllImport("gdi32.dll")] + private static extern uint GetPixel(IntPtr context, int x, int y); +} diff --git a/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs b/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs index 1d7c3fd..fdb6309 100644 --- a/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs +++ b/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs @@ -1,4 +1,6 @@ +using System.Globalization; using System.IO; +using System.Windows.Media; using System.Windows.Threading; using Autodesk.Revit.UI; using Nice3point.TUnit.Revit; @@ -209,4 +211,44 @@ await Assert.That(() => scope.Dispose(); }).ThrowsNothing(); } + + [Test] + public async Task BeginRenderingResumptionScope_TemplateOfThePreviousVersion_WindowDrawsTheNextFrame(CancellationToken cancellationToken) + { + // Arrange + var previousVersion = int.Parse(UiApplication.Application.VersionNumber, CultureInfo.InvariantCulture) - 1; + var templatePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Autodesk", $"RVT {previousVersion}", "Family Templates", "English", "Metric Detail Item.rft"); + if (!File.Exists(templatePath)) + { + Skip.Test($"Revit {previousVersion} with the English content is not installed"); + return; + } + + using var window = await ColorWindow.ShowAsync(Colors.Red, cancellationToken); + + // Act + using (RevitContext.BeginRenderingResumptionScope()) + { + UiApplication.Application.NewFamilyDocument(templatePath).Close(false); + } + + window.Fill(Colors.Blue); + + // Assert + await Assert.That(await window.WaitForDrawnColorAsync(Colors.Blue, cancellationToken)).IsEqualTo(Colors.Blue); + } + + [Test] + public async Task BeginRenderingResumptionScope_DisposedTwice_DoesNotThrow() + { + // Arrange + var scope = RevitContext.BeginRenderingResumptionScope(); + + // Act & Assert + await Assert.That(() => + { + scope.Dispose(); + scope.Dispose(); + }).ThrowsNothing(); + } } From 27ab0e88e14ccd7126fac3154d26805294f285a1 Mon Sep 17 00:00:00 2001 From: Nice3point Date: Sat, 31 Oct 2026 15:08:38 +0300 Subject: [PATCH 2/5] [Context] Replace the rendering resumption scope with a stateless WPF compatibility scope --- CHANGELOG.md | 4 +- Directory.Packages.props | 3 + README.md | 12 +- .../MediaContextNotificationWindow.cs | 57 +++++++++ .../Internal/NativeMethods.cs | 29 ----- .../NativeMethods.txt | 5 + .../Nice3point.Revit.Toolkit.csproj | 3 + .../Nice3point.Revit.Toolkit/RevitContext.cs | 81 ++++-------- .../ColorWindow.cs | 115 ++++++++---------- .../NativeMethods.json | 4 + .../NativeMethods.txt | 2 + .../Nice3point.Revit.Toolkit.Tests.csproj | 2 + .../RevitContextTests.cs | 15 ++- 13 files changed, 168 insertions(+), 164 deletions(-) create mode 100644 source/Nice3point.Revit.Toolkit/Internal/MediaContextNotificationWindow.cs delete mode 100644 source/Nice3point.Revit.Toolkit/Internal/NativeMethods.cs create mode 100644 source/Nice3point.Revit.Toolkit/NativeMethods.txt create mode 100644 tests/Nice3point.Revit.Toolkit.Tests/NativeMethods.json create mode 100644 tests/Nice3point.Revit.Toolkit.Tests/NativeMethods.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 03f9f57..24573a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,10 @@ ### Context -- New `RevitContext.BeginRenderingResumptionScope()` method resumes the rendering of the WPF windows of the Revit thread after Revit upgrades a family, a template, or a document of an earlier version. Previously, the progress dialog of the upgrade dropped the frame message of WPF, and the ribbon, the panes, and the windows of an add-in stopped drawing until a WPF window changed its size. +- New `RevitContext.BeginWpfCompatibilityScope()` method redraws the Revit interface after `Document.LoadFamily`, `Application.NewFamilyDocument`, or `Application.OpenDocumentFile` upgrades a family or a family template saved in an earlier Revit version. Without the scope, the ribbon, the dockable panes, and the add-in windows stop redrawing after the upgrade: they respond to clicks but show no change until a window is resized. ```c# -using (RevitContext.BeginRenderingResumptionScope()) +using (RevitContext.BeginWpfCompatibilityScope()) { document.LoadFamily(fileName, out var family); } diff --git a/Directory.Packages.props b/Directory.Packages.props index 5d641ef..b4d1dd6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -54,6 +54,9 @@ + + + diff --git a/README.md b/README.md index a88ef2f..0370617 100644 --- a/README.md +++ b/README.md @@ -834,20 +834,20 @@ using (RevitContext.BeginDialogSuppressionScope(args => } ``` -**Resuming rendering after a file upgrade** +**Loading families from earlier Revit versions** -Revit upgrades a family, a template, or a document of an earlier version when `Document.LoadFamily`, `Application.NewFamilyDocument`, or `Application.OpenDocumentFile` opens it. -While it upgrades, its progress dialog drops the message WPF waits for before it draws the next frame, and the ribbon, the panes, and the windows of an add-in stop drawing. -Wrap the calls that can upgrade a file in `BeginRenderingResumptionScope()` on the Revit thread: +After the API loads a family or creates one from a template saved in an earlier Revit version, the ribbon, the dockable panes, and the add-in windows stop redrawing. +They respond to clicks but show no change until a window is resized. +Wrap the calls in `BeginWpfCompatibilityScope()` on the Revit thread: ```C# -using (RevitContext.BeginRenderingResumptionScope()) +using (RevitContext.BeginWpfCompatibilityScope()) { document.LoadFamily(fileName, out var family); } ``` -The WPF windows of the thread draw again when the last active scope is disposed. +The interface redraws when the scope is disposed. ### Options diff --git a/source/Nice3point.Revit.Toolkit/Internal/MediaContextNotificationWindow.cs b/source/Nice3point.Revit.Toolkit/Internal/MediaContextNotificationWindow.cs new file mode 100644 index 0000000..e2a24b7 --- /dev/null +++ b/source/Nice3point.Revit.Toolkit/Internal/MediaContextNotificationWindow.cs @@ -0,0 +1,57 @@ +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace Nice3point.Revit.Toolkit.Internal; + +/// +/// Provides access to the window WPF creates on each UI thread to receive the notifications of its render thread. +/// +/// +/// The window title and the registered message name are internal to WPF and have the same values in .NET Framework and .NET. +/// +internal static class MediaContextNotificationWindow +{ + private const string WindowName = "MediaContextNotificationWindow"; + private static readonly uint ChannelNotifyMessage = PInvoke.RegisterWindowMessage("MilChannelNotify"); + + [ThreadStatic] private static HWND _window; + + /// + /// Posts the channel notification to the notification window of the current thread. + /// + /// + /// The method caches the window handle per thread after the first search. + /// A thread without a WPF dispatcher has no notification window, and the method posts nothing. + /// + public static void PostChannelNotification() + { + if (!_window.IsNull && PInvoke.PostMessage(_window, ChannelNotifyMessage, default, default)) + { + return; + } + + _window = FindThreadWindow(); + if (!_window.IsNull) + { + PInvoke.PostMessage(_window, ChannelNotifyMessage, default, default); + } + } + + /// + /// Finds the notification window of the current thread among the top-level windows of the desktop. + /// + /// The window handle, or a null handle when the thread has no notification window. + private static unsafe HWND FindThreadWindow() + { + var threadId = PInvoke.GetCurrentThreadId(); + var window = HWND.Null; + while (true) + { + window = PInvoke.FindWindowEx(HWND.Null, window, null, WindowName); + if (window.IsNull || PInvoke.GetWindowThreadProcessId(window, null) == threadId) + { + return window; + } + } + } +} diff --git a/source/Nice3point.Revit.Toolkit/Internal/NativeMethods.cs b/source/Nice3point.Revit.Toolkit/Internal/NativeMethods.cs deleted file mode 100644 index 3824a4a..0000000 --- a/source/Nice3point.Revit.Toolkit/Internal/NativeMethods.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Runtime.InteropServices; -using System.Text; - -namespace Nice3point.Revit.Toolkit.Internal; - -/// -/// Provides the Win32 functions the toolkit calls on the windows of a thread. -/// -internal static class NativeMethods -{ - internal delegate bool EnumThreadWindowsCallback(IntPtr window, IntPtr parameter); - - [DllImport("kernel32.dll")] - internal static extern uint GetCurrentThreadId(); - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool EnumThreadWindows(uint threadId, EnumThreadWindowsCallback callback, IntPtr parameter); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - internal static extern int GetWindowText(IntPtr window, StringBuilder text, int maxCount); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - internal static extern uint RegisterWindowMessage(string message); - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool PostMessage(IntPtr window, uint message, IntPtr wParam, IntPtr lParam); -} diff --git a/source/Nice3point.Revit.Toolkit/NativeMethods.txt b/source/Nice3point.Revit.Toolkit/NativeMethods.txt new file mode 100644 index 0000000..98a664f --- /dev/null +++ b/source/Nice3point.Revit.Toolkit/NativeMethods.txt @@ -0,0 +1,5 @@ +FindWindowEx +GetCurrentThreadId +GetWindowThreadProcessId +PostMessage +RegisterWindowMessage diff --git a/source/Nice3point.Revit.Toolkit/Nice3point.Revit.Toolkit.csproj b/source/Nice3point.Revit.Toolkit/Nice3point.Revit.Toolkit.csproj index 901a560..d2d32c9 100644 --- a/source/Nice3point.Revit.Toolkit/Nice3point.Revit.Toolkit.csproj +++ b/source/Nice3point.Revit.Toolkit/Nice3point.Revit.Toolkit.csproj @@ -4,6 +4,8 @@ true true $(DefineConstants);JETBRAINS_ANNOTATIONS + + $(NoWarn);PInvoke009 Debug.R20;Debug.R21;Debug.R22;Debug.R23;Debug.R24;Debug.R25;Debug.R26;Debug.R27 $(Configurations);Release.R20;Release.R21;Release.R22;Release.R23;Release.R24;Release.R25;Release.R26;Release.R27 @@ -36,6 +38,7 @@ + diff --git a/source/Nice3point.Revit.Toolkit/RevitContext.cs b/source/Nice3point.Revit.Toolkit/RevitContext.cs index e10be25..9e2c965 100644 --- a/source/Nice3point.Revit.Toolkit/RevitContext.cs +++ b/source/Nice3point.Revit.Toolkit/RevitContext.cs @@ -1,6 +1,5 @@ using System.Reflection; using System.Runtime.InteropServices; -using System.Text; using System.Windows; using Autodesk.Revit.UI; using Autodesk.Revit.UI.Events; @@ -26,13 +25,6 @@ public class RevitContext : RevitApiContext private static int? _suppressDialogCode; private static Action? _suppressDialogHandler; - private const string RenderNotificationWindowName = "MediaContextNotificationWindow"; - private static readonly uint RenderNotificationMessage = NativeMethods.RegisterWindowMessage("MilChannelNotify"); - private static readonly NativeMethods.EnumThreadWindowsCallback PostRenderNotificationCallback = PostRenderNotification; - private static readonly Lock RenderingLock = new(); - private static int _renderingScopeCount; - private static uint _renderingThreadId; - static RevitContext() { #if REVIT2027_OR_GREATER @@ -360,45 +352,36 @@ public static IDisposable BeginDialogSuppressionScope(Action - /// Begins a scope that resumes the rendering of the WPF windows of the current thread after Revit upgrades a file. - /// The rendering resumes when the outermost scope is disposed. + /// Begins a scope that redraws the Revit user interface after the API upgrades a family from an earlier Revit version. /// - /// A disposable scope. Call Dispose or use 'using' statement to resume the rendering. + /// A disposable scope. Call Dispose or use 'using' statement to redraw the user interface. /// /// - /// Revit upgrades a family, a template, or a document of an earlier version when - /// , , - /// or opens it. - /// While it upgrades, the progress dialog of Revit drops the message the WPF render thread posts after each frame, - /// and WPF presents no further frame of the thread until that message arrives: the ribbon, the panes, and the windows of an add-in stop drawing. + /// , , + /// and upgrade a family or a family template saved in an earlier Revit version, + /// and Revit shows the Model Upgrade progress dialog. + /// The dialog discards the notification WPF waits for before it draws the next frame. + /// After the upgrade, the ribbon, the dockable panes, and the WPF windows of the Revit thread respond to input but show no change until a window changes its size. + /// The model views are not affected. /// /// - /// Wrap the calls that can upgrade a file in the scope. - /// When the outermost scope is disposed, the scope posts the message again to the thread that began it, and the WPF windows of that thread draw again. - /// Begin the scope on the thread of the windows, the Revit main thread. + /// Disposing the scope posts the notification again, and the user interface redraws. + /// Begin the scope on the Revit thread. /// /// This method is thread-safe. /// /// /// - /// using (RevitContext.BeginRenderingResumptionScope()) + /// using (RevitContext.BeginWpfCompatibilityScope()) /// { /// document.LoadFamily(fileName, out var family); /// } - /// // The WPF windows draw again + /// // The user interface redraws /// /// - public static IDisposable BeginRenderingResumptionScope() + public static IDisposable BeginWpfCompatibilityScope() { - lock (RenderingLock) - { - if (_renderingScopeCount++ == 0) - { - _renderingThreadId = NativeMethods.GetCurrentThreadId(); - } - } - - return new RenderingResumptionScope(); + return WpfCompatibilityScope.Instance; } internal static IDisposable BeginApiContextScope() @@ -426,19 +409,6 @@ private static void ResolveDialogBox(object? sender, DialogBoxShowingEventArgs a handler?.Invoke(args); } - private static bool PostRenderNotification(IntPtr window, IntPtr parameter) - { - var name = new StringBuilder(RenderNotificationWindowName.Length + 2); - NativeMethods.GetWindowText(window, name, name.Capacity); - if (name.ToString() != RenderNotificationWindowName) - { - return true; - } - - NativeMethods.PostMessage(window, RenderNotificationMessage, IntPtr.Zero, IntPtr.Zero); - return false; - } - private sealed class ApiContextScope : IDisposable { private readonly IntPtr _deconstructorPointer; @@ -492,24 +462,19 @@ public void Dispose() } } - private sealed class RenderingResumptionScope : IDisposable + /// + /// Represents a scope that posts the frame notification of WPF to the current thread on every dispose. + /// + /// + /// WPF returns at once on a repeated notification, and every call of returns the same instance. + /// + private sealed class WpfCompatibilityScope : IDisposable { - private int _disposed; + public static readonly WpfCompatibilityScope Instance = new(); public void Dispose() { - if (Interlocked.Exchange(ref _disposed, value: 1) != 0) - { - return; - } - - lock (RenderingLock) - { - if (--_renderingScopeCount == 0) - { - NativeMethods.EnumThreadWindows(_renderingThreadId, PostRenderNotificationCallback, IntPtr.Zero); - } - } + MediaContextNotificationWindow.PostChannelNotification(); } } diff --git a/tests/Nice3point.Revit.Toolkit.Tests/ColorWindow.cs b/tests/Nice3point.Revit.Toolkit.Tests/ColorWindow.cs index 206fd95..d2120ad 100644 --- a/tests/Nice3point.Revit.Toolkit.Tests/ColorWindow.cs +++ b/tests/Nice3point.Revit.Toolkit.Tests/ColorWindow.cs @@ -1,20 +1,26 @@ -using System.Runtime.InteropServices; +using System.Drawing; using System.Windows; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Media.Animation; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Gdi; +using Windows.Win32.Storage.Xps; +using Brushes = System.Windows.Media.Brushes; using Color = System.Windows.Media.Color; +using HorizontalAlignment = System.Windows.HorizontalAlignment; using Rectangle = System.Windows.Shapes.Rectangle; namespace Nice3point.Revit.Toolkit.Tests; /// -/// A WPF window of the test thread filled with one color, outside the taskbar, with one column of pixels on the screen. -/// Windows composes no frame for a window entirely outside the screen. +/// Represents a WPF window of the test thread filled with one color, outside the taskbar, with one column of pixels on the screen. /// /// +/// Windows composes no frame for a window entirely outside the screen. /// The color the window shows is the color of the frame Windows composes for it, the last frame WPF presented. -/// A square in its corner pulses on every frame, as the windows of Revit draw while Revit works: Revit drops the frame message only while WPF draws. +/// A square in its corner pulses on every frame, as the ribbon and the panes of Revit draw while Revit works: Revit discards the frame notification only while WPF draws. /// internal sealed class ColorWindow : IDisposable { @@ -22,16 +28,14 @@ internal sealed class ColorWindow : IDisposable private const double VisibleWidth = 1; private const double PulseSize = 4; private const double PulseMinimumOpacity = 0.2; - private const uint RenderFullContent = 2; - private const int RedToGreenShift = 8; - private const int RedToBlueShift = 16; + private const PRINT_WINDOW_FLAGS RenderFullContent = (PRINT_WINDOW_FLAGS)2; private static readonly TimeSpan DrawTimeout = TimeSpan.FromSeconds(2); private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(50); private static readonly TimeSpan PulseDuration = TimeSpan.FromMilliseconds(500); private readonly Window _window; - private readonly IntPtr _handle; + private readonly HWND _handle; private ColorWindow(Color color) { @@ -65,12 +69,19 @@ private ColorWindow(Color color) _window.Content = pulse; _window.Show(); - _handle = new WindowInteropHelper(_window).Handle; + _handle = new HWND(new WindowInteropHelper(_window).Handle); } /// /// Shows the window and waits until it shows the color. /// + /// The color of the window. + /// A used to cancel the wait. + /// + /// A task that represents the asynchronous show operation. + /// The task result contains the shown window. + /// + /// The window shows another color when the time to draw a frame runs out. public static async Task ShowAsync(Color color, CancellationToken cancellationToken) { var window = new ColorWindow(color); @@ -84,14 +95,24 @@ public static async Task ShowAsync(Color color, CancellationToken c return window; } + /// + /// Fills the window with the color. + /// + /// The color of the window. public void Fill(Color color) { _window.Background = new SolidColorBrush(color); } /// - /// Returns the color the window shows once it is the expected color, or the color it shows when the time to draw a frame runs out. + /// Waits until the window shows the expected color, or until the time to draw a frame runs out. /// + /// The color to wait for. + /// A used to cancel the wait. + /// + /// A task that represents the asynchronous wait operation. + /// The task result contains the color the window shows. + /// public async Task WaitForDrawnColorAsync(Color expectedColor, CancellationToken cancellationToken) { var deadline = DateTime.UtcNow + DrawTimeout; @@ -105,70 +126,34 @@ public async Task WaitForDrawnColorAsync(Color expectedColor, Cancellatio return drawnColor; } + /// public void Dispose() { _window.Close(); } + /// + /// Reads the center pixel of the frame Windows composes for the window. + /// + /// The color of the center pixel. private Color ReadDrawnColor() { - GetClientRect(_handle, out var bounds); - var windowContext = GetDC(_handle); - var memoryContext = CreateCompatibleDC(windowContext); - var bitmap = CreateCompatibleBitmap(windowContext, bounds.Right, bounds.Bottom); - var previousObject = SelectObject(memoryContext, bitmap); - try + TestPInvoke.GetClientRect(_handle, out var bounds); + using var bitmap = new Bitmap(bounds.right, bounds.bottom); + using (var graphics = Graphics.FromImage(bitmap)) { - PrintWindow(_handle, memoryContext, RenderFullContent); - var pixel = GetPixel(memoryContext, bounds.Right / 2, bounds.Bottom / 2); - - return Color.FromRgb((byte)pixel, (byte)(pixel >> RedToGreenShift), (byte)(pixel >> RedToBlueShift)); - } - finally - { - SelectObject(memoryContext, previousObject); - DeleteObject(bitmap); - DeleteDC(memoryContext); - ReleaseDC(_handle, windowContext); + var context = graphics.GetHdc(); + try + { + TestPInvoke.PrintWindow(_handle, new HDC(context), RenderFullContent); + } + finally + { + graphics.ReleaseHdc(context); + } } - } - [StructLayout(LayoutKind.Sequential)] - private struct ClientBounds - { - public int Left; - public int Top; - public int Right; - public int Bottom; + var pixel = bitmap.GetPixel(bounds.right / 2, bounds.bottom / 2); + return Color.FromRgb(pixel.R, pixel.G, pixel.B); } - - [DllImport("user32.dll")] - private static extern bool GetClientRect(IntPtr window, out ClientBounds bounds); - - [DllImport("user32.dll")] - private static extern IntPtr GetDC(IntPtr window); - - [DllImport("user32.dll")] - private static extern int ReleaseDC(IntPtr window, IntPtr context); - - [DllImport("user32.dll")] - private static extern bool PrintWindow(IntPtr window, IntPtr context, uint flags); - - [DllImport("gdi32.dll")] - private static extern IntPtr CreateCompatibleDC(IntPtr context); - - [DllImport("gdi32.dll")] - private static extern IntPtr CreateCompatibleBitmap(IntPtr context, int width, int height); - - [DllImport("gdi32.dll")] - private static extern IntPtr SelectObject(IntPtr context, IntPtr graphicsObject); - - [DllImport("gdi32.dll")] - private static extern bool DeleteObject(IntPtr graphicsObject); - - [DllImport("gdi32.dll")] - private static extern bool DeleteDC(IntPtr context); - - [DllImport("gdi32.dll")] - private static extern uint GetPixel(IntPtr context, int x, int y); } diff --git a/tests/Nice3point.Revit.Toolkit.Tests/NativeMethods.json b/tests/Nice3point.Revit.Toolkit.Tests/NativeMethods.json new file mode 100644 index 0000000..1c96c39 --- /dev/null +++ b/tests/Nice3point.Revit.Toolkit.Tests/NativeMethods.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://aka.ms/CsWin32.schema.json", + "className": "TestPInvoke" +} diff --git a/tests/Nice3point.Revit.Toolkit.Tests/NativeMethods.txt b/tests/Nice3point.Revit.Toolkit.Tests/NativeMethods.txt new file mode 100644 index 0000000..65bb2d2 --- /dev/null +++ b/tests/Nice3point.Revit.Toolkit.Tests/NativeMethods.txt @@ -0,0 +1,2 @@ +GetClientRect +PrintWindow diff --git a/tests/Nice3point.Revit.Toolkit.Tests/Nice3point.Revit.Toolkit.Tests.csproj b/tests/Nice3point.Revit.Toolkit.Tests/Nice3point.Revit.Toolkit.Tests.csproj index c51569e..59baf50 100644 --- a/tests/Nice3point.Revit.Toolkit.Tests/Nice3point.Revit.Toolkit.Tests.csproj +++ b/tests/Nice3point.Revit.Toolkit.Tests/Nice3point.Revit.Toolkit.Tests.csproj @@ -4,6 +4,7 @@ Exe true false + true Debug.R21;Debug.R22;Debug.R23;Debug.R24;Debug.R25;Debug.R26;Debug.R27 $(Configurations);Release.R21;Release.R22;Release.R23;Release.R24;Release.R25;Release.R26;Release.R27 @@ -14,6 +15,7 @@ + diff --git a/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs b/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs index fdb6309..33b910a 100644 --- a/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs +++ b/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs @@ -213,7 +213,7 @@ await Assert.That(() => } [Test] - public async Task BeginRenderingResumptionScope_TemplateOfThePreviousVersion_WindowDrawsTheNextFrame(CancellationToken cancellationToken) + public async Task BeginWpfCompatibilityScope_TemplateOfThePreviousVersion_WindowDrawsTheNextFrame(CancellationToken cancellationToken) { // Arrange var previousVersion = int.Parse(UiApplication.Application.VersionNumber, CultureInfo.InvariantCulture) - 1; @@ -227,7 +227,7 @@ public async Task BeginRenderingResumptionScope_TemplateOfThePreviousVersion_Win using var window = await ColorWindow.ShowAsync(Colors.Red, cancellationToken); // Act - using (RevitContext.BeginRenderingResumptionScope()) + using (RevitContext.BeginWpfCompatibilityScope()) { UiApplication.Application.NewFamilyDocument(templatePath).Close(false); } @@ -239,10 +239,10 @@ public async Task BeginRenderingResumptionScope_TemplateOfThePreviousVersion_Win } [Test] - public async Task BeginRenderingResumptionScope_DisposedTwice_DoesNotThrow() + public async Task BeginWpfCompatibilityScope_DisposedTwice_DoesNotThrow() { // Arrange - var scope = RevitContext.BeginRenderingResumptionScope(); + var scope = RevitContext.BeginWpfCompatibilityScope(); // Act & Assert await Assert.That(() => @@ -251,4 +251,11 @@ await Assert.That(() => scope.Dispose(); }).ThrowsNothing(); } + + [Test] + public async Task BeginWpfCompatibilityScope_DisposedOnThreadWithoutWpf_DoesNotThrow() + { + // Act & Assert + await Assert.That(() => Task.Run(() => RevitContext.BeginWpfCompatibilityScope().Dispose())).ThrowsNothing(); + } } From ed56bbb529d4b6cfde960af85ada0a5ed62cceb4 Mon Sep 17 00:00:00 2001 From: Nice3point Date: Sat, 31 Oct 2026 15:13:29 +0300 Subject: [PATCH 3/5] [Docs] Require CsWin32 for the Win32 interop --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index 3ea03d1..15726ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,7 @@ The same package bundles Roslyn tooling: an incremental source generator that em * Model a temporary state change as an `IDisposable` returned from a `Begin...Scope()` factory that reverses it on `Dispose`. * Shared static state is thread-safe. * Reflection and native interop stay non-public in `Internal/`. +* CsWin32 generates the Win32 interop from the `NativeMethods.txt` of each project. A new Win32 API goes into that file, never into a hand-written `DllImport` or `LibraryImport`. * Never break the public surface. Deprecate a renamed member with `[Obsolete]` with a JetBrains `[CodeTemplate]` auto-conversion; the obsolete member forwards to the replacement and never throws or changes behavior. * Mark a member Revit invokes but consumers must not call `[EditorBrowsable(EditorBrowsableState.Never)]`. * The Roslyn tooling is public surface. It targets `netstandard2.0`, dual-targets Roslyn (the default with the `.Roslyn414` twins), uses stable `RVTTK####` diagnostic ids, and needs an `AnalyzerReleases.Unshipped.md` entry per new or changed rule. Never rename a generated member or renumber a shipped id. From 628752bdb52dfbe66a1fb1131e5519ea82e02dfb Mon Sep 17 00:00:00 2001 From: Nice3point Date: Sat, 31 Oct 2026 18:17:05 +0300 Subject: [PATCH 4/5] [Context] Rename the scope after the modal loop it compensates and announce it in the changelog --- CHANGELOG.md | 14 ++++++++++++-- README.md | 4 ++-- source/Nice3point.Revit.Toolkit/RevitContext.cs | 16 ++++++++-------- .../RevitContextTests.cs | 12 ++++++------ 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24573a1..d855abd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,15 +2,25 @@ ### Context -- New `RevitContext.BeginWpfCompatibilityScope()` method redraws the Revit interface after `Document.LoadFamily`, `Application.NewFamilyDocument`, or `Application.OpenDocumentFile` upgrades a family or a family template saved in an earlier Revit version. Without the scope, the ribbon, the dockable panes, and the add-in windows stop redrawing after the upgrade: they respond to clicks but show no change until a window is resized. +#### No more frozen Revit interface after loading older families + +It's a long-standing Revit issue known from the forums: an add-in loads a family saved in an earlier Revit version, and the Revit interface stops updating. +The ribbon, the dockable panes, and the add-in windows still react to clicks, but nothing redraws until you resize a window. + +The cause is the modal loop of the Model Upgrade progress dialog: it swallows the message WPF waits for before it draws the next frame. + +The new `RevitContext.BeginModalLoopCompatibilityScope()` fixes it. +Wrap the calls that load or open older files, and the interface redraws as soon as the scope ends: ```c# -using (RevitContext.BeginWpfCompatibilityScope()) +using (RevitContext.BeginModalLoopCompatibilityScope()) { document.LoadFamily(fileName, out var family); } ``` +The scope covers `Document.LoadFamily`, `Application.NewFamilyDocument`, and `Application.OpenDocumentFile`, and costs a single window message when it ends. + # 2027.0.3 ### External Events diff --git a/README.md b/README.md index 0370617..39e8c41 100644 --- a/README.md +++ b/README.md @@ -838,10 +838,10 @@ using (RevitContext.BeginDialogSuppressionScope(args => After the API loads a family or creates one from a template saved in an earlier Revit version, the ribbon, the dockable panes, and the add-in windows stop redrawing. They respond to clicks but show no change until a window is resized. -Wrap the calls in `BeginWpfCompatibilityScope()` on the Revit thread: +Wrap the calls in `BeginModalLoopCompatibilityScope()` on the Revit thread: ```C# -using (RevitContext.BeginWpfCompatibilityScope()) +using (RevitContext.BeginModalLoopCompatibilityScope()) { document.LoadFamily(fileName, out var family); } diff --git a/source/Nice3point.Revit.Toolkit/RevitContext.cs b/source/Nice3point.Revit.Toolkit/RevitContext.cs index 9e2c965..de4a9d9 100644 --- a/source/Nice3point.Revit.Toolkit/RevitContext.cs +++ b/source/Nice3point.Revit.Toolkit/RevitContext.cs @@ -359,8 +359,8 @@ public static IDisposable BeginDialogSuppressionScope(Action /// , , /// and upgrade a family or a family template saved in an earlier Revit version, - /// and Revit shows the Model Upgrade progress dialog. - /// The dialog discards the notification WPF waits for before it draws the next frame. + /// and Revit runs the modal loop of the Model Upgrade progress dialog. + /// The modal loop discards the notification WPF waits for before it draws the next frame. /// After the upgrade, the ribbon, the dockable panes, and the WPF windows of the Revit thread respond to input but show no change until a window changes its size. /// The model views are not affected. /// @@ -372,16 +372,16 @@ public static IDisposable BeginDialogSuppressionScope(Action /// /// - /// using (RevitContext.BeginWpfCompatibilityScope()) + /// using (RevitContext.BeginModalLoopCompatibilityScope()) /// { /// document.LoadFamily(fileName, out var family); /// } /// // The user interface redraws /// /// - public static IDisposable BeginWpfCompatibilityScope() + public static IDisposable BeginModalLoopCompatibilityScope() { - return WpfCompatibilityScope.Instance; + return ModalLoopCompatibilityScope.Instance; } internal static IDisposable BeginApiContextScope() @@ -466,11 +466,11 @@ public void Dispose() /// Represents a scope that posts the frame notification of WPF to the current thread on every dispose. /// /// - /// WPF returns at once on a repeated notification, and every call of returns the same instance. + /// WPF returns at once on a repeated notification, and every call of returns the same instance. /// - private sealed class WpfCompatibilityScope : IDisposable + private sealed class ModalLoopCompatibilityScope : IDisposable { - public static readonly WpfCompatibilityScope Instance = new(); + public static readonly ModalLoopCompatibilityScope Instance = new(); public void Dispose() { diff --git a/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs b/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs index 33b910a..4369040 100644 --- a/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs +++ b/tests/Nice3point.Revit.Toolkit.Tests/RevitContextTests.cs @@ -213,7 +213,7 @@ await Assert.That(() => } [Test] - public async Task BeginWpfCompatibilityScope_TemplateOfThePreviousVersion_WindowDrawsTheNextFrame(CancellationToken cancellationToken) + public async Task BeginModalLoopCompatibilityScope_TemplateOfThePreviousVersion_WindowDrawsTheNextFrame(CancellationToken cancellationToken) { // Arrange var previousVersion = int.Parse(UiApplication.Application.VersionNumber, CultureInfo.InvariantCulture) - 1; @@ -227,7 +227,7 @@ public async Task BeginWpfCompatibilityScope_TemplateOfThePreviousVersion_Window using var window = await ColorWindow.ShowAsync(Colors.Red, cancellationToken); // Act - using (RevitContext.BeginWpfCompatibilityScope()) + using (RevitContext.BeginModalLoopCompatibilityScope()) { UiApplication.Application.NewFamilyDocument(templatePath).Close(false); } @@ -239,10 +239,10 @@ public async Task BeginWpfCompatibilityScope_TemplateOfThePreviousVersion_Window } [Test] - public async Task BeginWpfCompatibilityScope_DisposedTwice_DoesNotThrow() + public async Task BeginModalLoopCompatibilityScope_DisposedTwice_DoesNotThrow() { // Arrange - var scope = RevitContext.BeginWpfCompatibilityScope(); + var scope = RevitContext.BeginModalLoopCompatibilityScope(); // Act & Assert await Assert.That(() => @@ -253,9 +253,9 @@ await Assert.That(() => } [Test] - public async Task BeginWpfCompatibilityScope_DisposedOnThreadWithoutWpf_DoesNotThrow() + public async Task BeginModalLoopCompatibilityScope_DisposedOnThreadWithoutWpf_DoesNotThrow() { // Act & Assert - await Assert.That(() => Task.Run(() => RevitContext.BeginWpfCompatibilityScope().Dispose())).ThrowsNothing(); + await Assert.That(() => Task.Run(() => RevitContext.BeginModalLoopCompatibilityScope().Dispose())).ThrowsNothing(); } } From 96761852f96c090eb1e2eb8f420c52d42d8dbdf6 Mon Sep 17 00:00:00 2001 From: Roman Date: Sat, 31 Oct 2026 18:24:02 +0300 Subject: [PATCH 5/5] Update Nice3point.Revit.Toolkit.csproj --- .../Nice3point.Revit.Toolkit/Nice3point.Revit.Toolkit.csproj | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/source/Nice3point.Revit.Toolkit/Nice3point.Revit.Toolkit.csproj b/source/Nice3point.Revit.Toolkit/Nice3point.Revit.Toolkit.csproj index d2d32c9..373f6ad 100644 --- a/source/Nice3point.Revit.Toolkit/Nice3point.Revit.Toolkit.csproj +++ b/source/Nice3point.Revit.Toolkit/Nice3point.Revit.Toolkit.csproj @@ -4,10 +4,9 @@ true true $(DefineConstants);JETBRAINS_ANNOTATIONS - - $(NoWarn);PInvoke009 Debug.R20;Debug.R21;Debug.R22;Debug.R23;Debug.R24;Debug.R25;Debug.R26;Debug.R27 $(Configurations);Release.R20;Release.R21;Release.R22;Release.R23;Release.R24;Release.R25;Release.R26;Release.R27 + $(NoWarn);PInvoke009 @@ -67,4 +66,4 @@ - \ No newline at end of file +