Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 115 additions & 129 deletions src/Splat.Drawing/DefaultPlatformModeDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,70 +2,89 @@
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

#if !MONO
using System.Diagnostics.CodeAnalysis;
#endif
using System.IO;
#if NETFRAMEWORK
using System.Reflection;
#endif

namespace Splat;

/// <summary>
/// Provides a default implementation for detecting whether the application is running in design mode across supported
/// platforms.
/// </summary>
/// <remarks>This class is typically used to determine if code is executing within a designer environment, such as
/// Visual Studio or Blend, to enable or disable design-time specific logic. It supports multiple platforms and design
/// environments, including WPF, Silverlight, and UWP, by checking for known design mode indicators. The detection
/// result may be cached for performance. Thread safety is not guaranteed.</remarks>
/// <summary>Provides a default implementation for detecting whether the application is running in design mode.</summary>
/// <remarks>
/// <para>This class is typically used to determine if code is executing within a designer environment, such as
/// Visual Studio or Blend, to enable or disable design-time specific logic. The detection result is memoized, so a
/// process that starts outside a designer never re-runs the probes. Thread safety is not guaranteed.</para>
/// <para>Which probes are compiled depends on the target framework. Splat.Drawing sets <c>UseWPF</c> and
/// <c>UseWindowsForms</c> for the .NET Framework and Windows-specific targets, so those builds call the WPF designer
/// API directly instead of reflecting over it. The target-framework-neutral builds keep a reflective probe, because a
/// Windows desktop application resolves to them when its own platform version predates the Windows-specific targets.
/// The Android and Apple targets cannot host a XAML designer at all and compile no designer probe.</para>
/// </remarks>
public class DefaultPlatformModeDetector : IPlatformModeDetector
{
/// <summary>Assembly-qualified name of the Silverlight/XAML <c>DesignerProperties</c> type probed for design mode.</summary>
private const string XamlDesignPropertiesType = "System.ComponentModel.DesignerProperties, System.Windows, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e";

/// <summary>Assembly-qualified name of the XAML <c>Border</c> control used as the dependency object for the design-mode probe.</summary>
private const string XamlControlBorderType = "System.Windows.Controls.Border, System.Windows, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e";

/// <summary>Name of the XAML <c>DesignerProperties</c> method that reports design mode.</summary>
private const string XamlDesignPropertiesDesignModeMethodName = "GetIsInDesignMode";

#if !NETFRAMEWORK && !WINDOWS && !MONO
/// <summary>Assembly-qualified name of the WPF <c>DesignerProperties</c> type probed for design mode.</summary>
private const string WpfDesignerPropertiesType = "System.ComponentModel.DesignerProperties, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35";

/// <summary>Name of the WPF <c>DesignerProperties</c> method that reports design mode.</summary>
private const string WpfDesignerPropertiesDesignModeMethod = "GetIsInDesignMode";

/// <summary>Assembly-qualified name of the WPF <c>DependencyObject</c> type used as the dependency object for the design-mode probe.</summary>
private const string WpfDependencyPropertyType = "System.Windows.DependencyObject, WindowsBase, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35";
/// <summary>Assembly-qualified name of the WPF element type the designer raises the design-mode default for.</summary>
private const string WpfFrameworkElementType = "System.Windows.FrameworkElement, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35";
#endif

/// <summary>Assembly-qualified name of the WinRT <c>DesignMode</c> type probed for design mode.</summary>
private const string WinFormsDesignerPropertiesType = "Windows.ApplicationModel.DesignMode, Windows, ContentType=WindowsRuntime";
#if !MONO
/// <summary>Assembly-qualified name of the Windows Runtime <c>DesignMode</c> type probed for design mode.</summary>
private const string WinRtDesignModeType = "Windows.ApplicationModel.DesignMode, Windows, ContentType=WindowsRuntime";

/// <summary>Name of the WinRT <c>DesignMode</c> property that reports design mode.</summary>
private const string WinFormsDesignerPropertiesDesignModeMethod = "DesignModeEnabled";
/// <summary>Name of the Windows Runtime <c>DesignMode</c> property that reports design mode.</summary>
private const string WinRtDesignModeEnabledProperty = "DesignModeEnabled";
#endif

/// <summary>Executable names of known design-time host processes used as a fallback design-mode signal.</summary>
private static readonly string[] _designEnvironments = ["BLEND.EXE", "XDESPROC.EXE"];

/// <summary>Memoizes the design-mode detection result; <see langword="null"/> until first computed.</summary>
private static bool? _cachedInDesignModeResult;

/// <summary>Gets the path of the executable hosting the current process, or <see langword="null"/> when it is unavailable.</summary>
/// <remarks>This is the executable file, not the directory the assembly was loaded from. A design-time host is
/// recognised by its process name, and a directory path yields no executable name to compare.</remarks>
internal static string? HostExecutablePath =>
#if NETFRAMEWORK
Assembly.GetEntryAssembly()?.Location;
#else
Environment.ProcessPath;
#endif

/// <inheritdoc />
public bool? InDesignMode() => DetectDesignMode();
public bool? InDesignMode() => _cachedInDesignModeResult ??= DetectDesignMode(HostExecutablePath);

/// <summary>Determines whether the supplied host entry-point path names a known design-environment executable.</summary>
/// <param name="entry">The host entry-point path, or <see langword="null"/> when it is unavailable.</param>
/// <returns><see langword="true"/> when the entry path names a known design environment; otherwise <see langword="false"/>.</returns>
/// <remarks>The executable name has to match a known design host exactly. A containment test reports a false
/// positive for every name that is a fragment of one, including the empty name a directory path yields.</remarks>
internal static bool IsDesignEnvironmentEntry(string? entry)
{
if (entry is null)
if (string.IsNullOrEmpty(entry))
{
return false;
}

var exeName = new FileInfo(entry).Name;
var executableName = new FileInfo(entry).Name;

foreach (var designEnv in _designEnvironments)
if (executableName.Length == 0)
{
if (IsDesignEnvironment(designEnv, exeName))
return false;
}

foreach (var knownHost in _designEnvironments)
{
if (string.Equals(knownHost, executableName, StringComparison.OrdinalIgnoreCase))
{
return true;
}
Expand All @@ -74,118 +93,85 @@ internal static bool IsDesignEnvironmentEntry(string? entry)
return false;
}

/// <summary>Runs the design-mode probes in priority order and returns (and caches) the result.</summary>
/// <returns>A value indicating whether the application is running in design mode.</returns>
private static bool? DetectDesignMode()
{
if (_cachedInDesignModeResult.HasValue)
{
return _cachedInDesignModeResult.Value;
}

// Probe each platform in priority order; the first detected platform wins,
// mirroring the original else-if chain. The cached result is then reset to
// false below, preserving the original behaviour.
RunDesignModeProbes();

_cachedInDesignModeResult = false;
/// <summary>Runs the design-mode probes and reports whether any of them detected a designer.</summary>
/// <param name="hostExecutablePath">The path of the executable hosting the current process, or <see langword="null"/>.</param>
/// <returns><see langword="true"/> when a designer is hosting the application; otherwise <see langword="false"/>.</returns>
internal static bool DetectDesignMode(string? hostExecutablePath) =>
#if MONO
IsDesignEnvironmentEntry(hostExecutablePath);
#else
ResolveDesignMode(ProbeWpfDesignMode(), ProbeWinRtDesignMode(), hostExecutablePath);
#endif

return _cachedInDesignModeResult;
}
#if !MONO
/// <summary>Combines the probe results into a single answer: any probe reporting a designer wins.</summary>
/// <param name="wpfDesignMode">Whether the WPF probe reported a designer.</param>
/// <param name="winRtDesignMode">Whether the Windows Runtime probe reported a designer.</param>
/// <param name="hostExecutablePath">The path of the executable hosting the current process, or <see langword="null"/>.</param>
/// <returns><see langword="true"/> when a designer is hosting the application; otherwise <see langword="false"/>.</returns>
internal static bool ResolveDesignMode(bool wpfDesignMode, bool winRtDesignMode, string? hostExecutablePath) =>
wpfDesignMode || winRtDesignMode || IsDesignEnvironmentEntry(hostExecutablePath);
#endif

/// <summary>Runs the platform design-mode probes in priority order.</summary>
/// <remarks>None of the Silverlight, WPF, or WinRT host types resolve on the supported .NET targets, so the
/// short-circuiting branches that fire when one of those probes reports a platform are unreachable off their
/// native hosts.</remarks>
[ExcludeFromCodeCoverage]
private static void RunDesignModeProbes() =>
_ = ProbeXamlDesignProperties()
|| ProbeWpfDesignerProperties()
|| ProbeWinRtDesignMode()
|| ProbeDesignEnvironmentExecutable();

/// <summary>Probes for the Silverlight / Windows Phone 8 design-mode indicator.</summary>
/// <returns><see langword="true"/> if the platform was detected; otherwise <see langword="false"/>.</returns>
[ExcludeFromCodeCoverage] // Off-platform reflection: the Silverlight/XAML type never resolves on supported targets; only the type-null guard runs.
private static bool ProbeXamlDesignProperties()
/// <summary>Gets the memoized design-mode result. Used by test scopes.</summary>
/// <returns>The memoized result, or <see langword="null"/> when the probes have not run yet.</returns>
internal static bool? GetState() => _cachedInDesignModeResult;

/// <summary>Restores the memoized design-mode result. Used by test scopes.</summary>
/// <param name="state">The memoized result to restore.</param>
internal static void RestoreState(bool? state) => _cachedInDesignModeResult = state;

/// <summary>Discards the memoized design-mode result so the next query re-runs the probes. Used by test scopes.</summary>
internal static void ResetState() => _cachedInDesignModeResult = null;

#if !MONO
#if NETFRAMEWORK || WINDOWS
/// <summary>Reads the design-mode default that the WPF designer raises for the elements it loads.</summary>
/// <returns><see langword="true"/> when a XAML designer is hosting the application; otherwise <see langword="false"/>.</returns>
/// <remarks>The designer raises the default that <c>FrameworkElement</c> reports for the <c>IsInDesignMode</c>
/// attached property, which is what makes every element it loads answer <see langword="true"/>. Asking
/// <c>DesignerProperties.GetIsInDesignMode</c> about a freshly constructed, unparented <c>DependencyObject</c>
/// reads the unraised base default instead, so it answers <see langword="false"/> for any control the designer did
/// not set the attached property on directly - a user control nested inside another user control, for example.
/// Reading the metadata needs no element at all, so it also avoids creating a dispatcher-affine object.</remarks>
[ExcludeFromCodeCoverage] // Only a hosting designer raises this default, and the call that would simulate it mutates the property metadata for the whole process.
private static bool ProbeWpfDesignMode() =>
System.ComponentModel.DesignerProperties.IsInDesignModeProperty
.GetMetadata(typeof(System.Windows.FrameworkElement)).DefaultValue is true;
#else
/// <summary>Reads the design-mode default that the WPF designer raises, when WPF is loaded into the process.</summary>
/// <returns><see langword="true"/> when a XAML designer is hosting the application; otherwise <see langword="false"/>.</returns>
/// <remarks>The element handed to <c>GetIsInDesignMode</c> is a <c>FrameworkElement</c> rather than a bare
/// <c>DependencyObject</c>, because the designer raises the attached property's default for
/// <c>FrameworkElement</c>. A bare <c>DependencyObject</c> reads the unraised base default and therefore answers
/// <see langword="false"/> for any control the designer did not set the attached property on directly - a user
/// control nested inside another user control, for example.</remarks>
[ExcludeFromCodeCoverage] // Off-platform reflection: PresentationFramework only resolves inside a WPF host.
private static bool ProbeWpfDesignMode()
{
var type = Type.GetType(XamlDesignPropertiesType, false);
if (type is null)
{
return false;
}
var designerProperties = Type.GetType(WpfDesignerPropertiesType, false);

var methodInfo = type.GetMethod(XamlDesignPropertiesDesignModeMethodName);
var dependencyObject = Type.GetType(XamlControlBorderType, false);

if (methodInfo is null || dependencyObject is null)
{
return true;
}

_cachedInDesignModeResult = (bool)(methodInfo.Invoke(null, [Activator.CreateInstance(dependencyObject)]) ?? false);
return true;
}

/// <summary>Probes for the WPF designer design-mode indicator.</summary>
/// <returns><see langword="true"/> if the platform was detected; otherwise <see langword="false"/>.</returns>
[ExcludeFromCodeCoverage] // Off-platform reflection: the WPF type never resolves on supported targets; only the type-null guard runs.
private static bool ProbeWpfDesignerProperties()
{
var type = Type.GetType(WpfDesignerPropertiesType, false);
if (type is null)
if (designerProperties is null)
{
return false;
}

var methodInfo = type.GetMethod(WpfDesignerPropertiesDesignModeMethod);
var dependencyObject = Type.GetType(WpfDependencyPropertyType, false);
if (methodInfo is null || dependencyObject is null)
{
return true;
}

_cachedInDesignModeResult = (bool)(methodInfo.Invoke(null, [Activator.CreateInstance(dependencyObject)]) ?? false);
return true;
}

/// <summary>Probes for the WinRT design-mode indicator.</summary>
/// <returns><see langword="true"/> if the platform was detected; otherwise <see langword="false"/>.</returns>
[ExcludeFromCodeCoverage] // Off-platform reflection: the WinRT type never resolves on supported targets; only the type-null guard runs.
private static bool ProbeWinRtDesignMode()
{
var type = Type.GetType(WinFormsDesignerPropertiesType, false);
if (type is null)
{
return false;
}
var designModeMethod = designerProperties.GetMethod(WpfDesignerPropertiesDesignModeMethod);
var frameworkElement = Type.GetType(WpfFrameworkElementType, false);

_cachedInDesignModeResult = (bool)(type.GetProperty(WinFormsDesignerPropertiesDesignModeMethod)?.GetMethod?.Invoke(null, null) ?? false);
return true;
return designModeMethod is not null
&& frameworkElement is not null
&& designModeMethod.Invoke(null, [Activator.CreateInstance(frameworkElement)]) is true;
}

/// <summary>Probes for a known design-environment host executable as the fallback indicator.</summary>
/// <returns>Always <see langword="true"/>, as this is the terminal fallback probe.</returns>
private static bool ProbeDesignEnvironmentExecutable()
{
#if NETFRAMEWORK
var entry = Assembly.GetEntryAssembly()?.Location;
#else
var entry = AppContext.BaseDirectory;
#endif
_cachedInDesignModeResult = IsDesignEnvironmentEntry(entry);

return true;
}

/// <summary>Determines whether the host executable name corresponds to a known design environment.</summary>
/// <param name="designEnvironment">The known design-environment executable name.</param>
/// <param name="exeName">The current host executable name.</param>
/// <returns><see langword="true"/> when the host is the supplied design environment.</returns>
private static bool IsDesignEnvironment(string designEnvironment, string exeName) =>
#if NETFRAMEWORK
designEnvironment.IndexOf(exeName, StringComparison.InvariantCultureIgnoreCase) != -1;
#else
designEnvironment.Contains(exeName, StringComparison.InvariantCultureIgnoreCase);
/// <summary>Reads the Windows Runtime design-mode indicator, when the Windows Runtime is available.</summary>
/// <returns><see langword="true"/> when a Windows Runtime designer is hosting the application; otherwise <see langword="false"/>.</returns>
[ExcludeFromCodeCoverage] // Off-platform reflection: the Windows Runtime projection only resolves inside a Windows Runtime host.
private static bool ProbeWinRtDesignMode() =>
Type.GetType(WinRtDesignModeType, false)?
.GetProperty(WinRtDesignModeEnabledProperty)?
.GetMethod?
.Invoke(null, null) is true;
#endif
}
12 changes: 8 additions & 4 deletions src/Splat.Drawing/PlatformModeDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,26 @@ public static bool InDesignMode()
}

/// <summary>Gets the current state for test isolation. Used by test scopes.</summary>
/// <returns>A tuple containing the current detector and cached result.</returns>
internal static (IPlatformModeDetector detector, bool? cachedResult) GetState() =>
(Current, _cachedInDesignModeResult);
/// <returns>A tuple containing the current detector, the cached result, and the default detector's own cached result.</returns>
/// <remarks><see cref="DefaultPlatformModeDetector"/> memoizes separately, so its cache has to travel with this
/// state; otherwise a test that leaves a design-mode value behind there leaks into every later test.</remarks>
internal static (IPlatformModeDetector detector, bool? cachedResult, bool? defaultDetectorCachedResult) GetState() =>
(Current, _cachedInDesignModeResult, DefaultPlatformModeDetector.GetState());

/// <summary>Restores the state for test isolation. Used by test scopes.</summary>
/// <param name="state">The state to restore.</param>
internal static void RestoreState((IPlatformModeDetector detector, bool? cachedResult) state)
internal static void RestoreState((IPlatformModeDetector detector, bool? cachedResult, bool? defaultDetectorCachedResult) state)
{
Current = state.detector;
_cachedInDesignModeResult = state.cachedResult;
DefaultPlatformModeDetector.RestoreState(state.defaultDetectorCachedResult);
}

/// <summary>Resets the state to default for test isolation. Used by test scopes.</summary>
internal static void ResetState()
{
Current = new DefaultPlatformModeDetector();
_cachedInDesignModeResult = null;
DefaultPlatformModeDetector.ResetState();
}
}
Loading
Loading