Owner Igor Velikorossov
Historically Windows Forms designer was rendering its design surface based on hardcoded assumptions, such as control default font and dpi settings. However over the past 10-15 years not only multi-monitor configurations became a norm, monitor tech has made significant progress in making 4K, 5K, 8K, etc. monitors widely used by our consumers. The Windows itself has been innovating and tinkering with UI configurations, such default font metrics (which over the years deviated from "Microsoft Sans Serif, 8pt" to "Segoe UI, 9pt"). Incidentally the office team is currently in search of the next default font, and Windows 11 may have yet another default font too.
The .NET Core/.NET-powered Windows Forms runtime has been (more or less) keeping up with the changes, e.g. by providing new API to set high dpi mode, or by updating the default font. The default font change has unearthed numerous issues in the runtime (e.g. different font-based scaling), which we addressed by providing (yet another) API to set an application-wide default font.
However during numerous discussions the team has identified probably the biggest flaw in the current separation of the runtime and the designer, and the overall move from .NET Framework assumptions - the lack of a mechanism to share Windows Forms project-level configuration settings between the runtime and the designer. That is, if the app is configured to run with disabled visual styles, in PerMonitorV2, and have an application-wide default font set to "Arial, 14pt" there is no way to show the designer surface with the same settings to truly provide the WYSIWYG experience.
The purpose of this proposal is to:
- streamline a Windows Forms application configuration and bootstrap,
- with the view that this will facilitate the sharing of the information between the runtime and the designer during the development phase.
That is, whenever the designers surface process is started configuration information is read from a known location, and necessary configurations are applied (e.g. run the design surface in PerMonitorV2 mode, or set a form/usercontrol default font to "Arial, 14pt").
NOTE: The new functionality is opt-in, i.e. unless a developer makes a conscious decision to use the new configuration and the bootstrap mechanism existing applications will continue to work as-is, and the current developer experience will remain the same.
Refer to Design section below.
-
The new bootstrap experience (this includes source generators and analyzers) must come inbox with the Windows Desktop SDK, and be available without any additional references or NuGet packages from get go (i.e.
dotnet new winforms && dotnet build
).
Related: dotnet/designs#181 -
The shared configuration must be stored in a way that is easily accessible to the Widnows Forms Designer and Windows Forms runtime. And this storage mechanism must not have any adverse effect on the perfomance of the Designer, i.e. not require any additional computation, unless absolutely necessary.
-
The shared configuration must work for single project and multi project scenarios, i.e. for scenarios where
- the application entry point (i.e.
Main()
) and all user forms and controls reside in the same project, and - the application entry point (i.e.
Main()
) resides in one project, and user forms and controls either reside in other projects within the solution or referenced via NuGet packages.
- the application entry point (i.e.
-
The configuration defaults must be chosen such that the original code in
Main()
could be replaced with the new bootstrap call, and no further configuration would be required. -
The Windows Forms application template must be updated with the new bootstrap experiece.
-
Update Visual Studio property page for Windows Forms projects.
-
Build the following Roslyn Analyzers functions:
- Check for invocations of now-redundant
Applicaiton.*
methods invoked byApplicationConfiguration.Initialize()
. - If a custom app.manifest is specified, parse it, and if dpi-related settings are found - warn the user, and direct to supply the dpi mode via the MSBuild property defined below.
- (Consider) checking for app.config and dpi-related configurations, if found - warn the user, and direct to supply the dpi mode via the MSBuild property defined below.
- Check if
Application.SetHighDpiMode()
is invoked with anything other thanHighDpiMode.SystemAware
(see: dotnet/winforms-designer#3278)
- Check for invocations of now-redundant
-
Update the Windows Forms application template using the top level statements syntax.
-
Provide source generator/analyzer support for apps built on .NET Framework, .NET Core 3.1, or .NET 5.0.
-
Design/implement top level statements fo Windows Forms applications. Tracked under: dotnet/winforms#5074
-
Design/implement new host/builder model for Windows Forms applications.
-
Migrate Visual Basic apps off the Application Framework or change the use of *.myapp file.
-
Streamline the bootstrap of Visual Basic apps or use Roslyn source generators for this purpose.
-
Use Roslyn analyzers in Visual Basic scenarios until previous two items are addressed.
-
Provide a mechanism to configure locale aware font descriptors.
-
Provide a mechanism to configure fonts that are vertical or require custom GDI charsets, or are part of
SystemFonts
collection.
🤔 In the future releases of .NET (e.g. 7.0+) we will consider how to migrate Visual Basic off the Application Framework and *.myapp file in favour of Roslyn source generators and MSBuild properties. This could also significantly reduce efforts in maintaining Visual Studio property pages for Visual Basic projects (e.g. dotnet/project-system#7236, dotnet/project-system#7240, and dotnet/project-system#7241)
- Windows Forms runtime/designer: @dotnet/dotnet-winforms @OliaG @DustinCampbell
- Visual Basic: @KathleenDollard @KlausLoeffelmann
- Roslyn: @jaredpar @sharwell
- Project System: @drewnoakes
- SDK: dsplaisted
- .NET libraries: @ericstj
- General: @terrajobst @Pilchie
Ironically Visual Basic apps are in a good position for the designer integration with their Application Framework functionality, i.e. "*.myapp" file:
The designer surface process should have no issues reading configuration values from a myapp file. Any work further work at this stage pertaining for streamlining the bootstrap is out of scope of this design proposal.
static class Program
{
[STAThread]
static void Main()
{
+ ApplicationConfiguration.Initialize();
- Application.EnableVisualStyles();
- Application.SetCompatibleTextRenderingDefault(false);
- Application.SetDefaultFont(new Font(....));
- Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.Run(new MainForm());
}
}
New properties:
// proj
<PropertyGroup>
<ApplicationIcon />
<ApplicationManifest>app1.manifest</ApplicationManifest>
+ <!--
+ Purpose: controls whether to emit: Application.EnableVisualStyles();
+ Default=true
+ Empty value=true
+ -->
+ <ApplicationVisualStyles>[true|false]</ApplicationVisualStyles>
+ <!--
+ Purpose: the value in: Application.SetCompatibleTextRenderingDefault(...);
+ Default=false
+ Empty value=false
+ -->
+ <ApplicationUseCompatibleTextRendering>[true|false]</ApplicationUseCompatibleTextRendering>
+ <!--
+ Purpose: contains a custom font information; controls whether to emit: Application.SetDefaultFont(new Font(....));
+ Default=''
+ Empty value='', implies Control.DefaultFont
+ -->
+ <ApplicationDefaultFont>[equivalent to FontConverter.ConvertToInvariantString()]</ApplicationDefaultFont>
+ <!--
+ Purpose: the value in: Application.SetHighDpiMode(...);
+ Default=SystemAware
+ Empty value=SystemAware
+ -->
+ <ApplicationHighDpiMode>[string/HighDpiMode enum value]</ApplicationHighDpiMode>
</PropertyGroup>
Existing properties of interest:
// proj
<PropertyGroup>
<OutputType>WinExe</OutputType>
<ApplicationManifest>[manifest file, optional, default='']</ApplicationManifest>
</PropertyGroup>
-
The new bootstrap API must only work for Windows applications projects (i.e.
OutputType = WinExe
andOutputType = Exe
) because:- These projects have an entry point, where an app is initialised; and
- they also specify an application manifest, if there is one.
During compilation Roslyn source generators will read supplied configurations, and emit code for the necessary API, e.g.
⚠️ The screenshot is dated, provided for concept demonstration purposes only:
-
There are multiple possible places for storing values that must be accessible to the Designer, and the Designer must be able to read these values quickly and efficiently. The contenders are:
Pros Cons Csproj/props (vbproj in the long run) - Easy to add/modify
- Accessible to all projects in the solution (multi project scenario)
- Can be overriden at a project level (multi project scenario)
- Easy to expose in the Visual Studio project settings UI
- "After all we’ve done to clean up project files, it feels like a regression to add properties that don’t need to be there. MSBuild files are already hard enough for many users to understand, and we won’t be helping by putting values there that doesn’t really make sense to be there."
Code (presumably in WinExe) as a known to the designer method (akin to InitializeComponent()
) or attributes- Generally familiar model to C# devs
- Won't work in multi project scenarios
- Likely have an adverse effect on the designer start up - the designer needs to load and parse the method/attributes data
- Very difficult to expose in the Visual Studio project settings UI
app.json/project.json/app.config/etc. - Easy to add/modify
- Not "polluting" csproj/props with unrelated properties
- It is an anti-pattern, we shouldn't inventing new files because we don’t want to clutter the project files.
- Configs may not be accessible to all projects in the solution (multi project scenario)
- app.config don't appear to be used in .NET, and our templates no longer carry one
- May be difficult to expose in the Visual Studio project settings UI
.editorconfig - Easy to add/modify
- Accessible to all projects in the solution (multi project scenario)
- There is an existing Visual Studio UI
- The proposed configuration is not a style configuration, nor exclusive to design-time (for WinExe, though is for control libraries)
After a number of discussions, it was concluded that MSBuild properties (i.e. csproj/props) is the best mechanism for storing settings. The concerns with polluting project files can be addressed with sensible defaults, that is a "zero configuration" must work for the main stream scenarios, and ultimately produce the same code being replaced:
static class Program { [STAThread] static void Main() { + ApplicationConfiguration.Initialize(); - Application.EnableVisualStyles(); // ApplicationVisualStyles='' or true - Application.SetCompatibleTextRenderingDefault(false); // ApplicationUseCompatibleTextRendering='' or false - Application.SetHighDpiMode(HighDpiMode.SystemAware); // ApplicationHighDpiMode='' or SystemAware - Application.SetDefaultFont(new Font(....)); // New in .NET 6.0 Preview5 - // ApplicationDefaultFont='' -> Control.DefaultFont Application.Run(new MainForm()); } }
-
We will be able to leverage the power of Roslyn analyzers to warn developers about duplicate/redundant API invocations performed outside the generated code, unnecessary DPI settings in the app.manifest (inspecting
ApplicationManifest
property), and generally steer them towards the new bootstrap experience.
The designer will respect the custom configurations (font for now, other may follow), and render the design surface with required metrics.
Tracked under dotnet/winforms-designer#3192 and dotnet/winforms-designer#3509.
To complete the user experience we can update the Visual Studio property page for C# Windows Forms projects, and provide an Application Framework-esque (yet arguably more refined) experience to C# developers. It can looks something like this mockup:
Tracked under dotnet/project-system#7279
After deliberations and discussions we propose the settings to be stored as MSBuild properties for the following reasons:
- The Windows Forms designer run in Visual Studio and able to read MSBuild properties.
- Unlike schema-enforced app.config, MSBuild properties are stored in an untyped propertybag.
- app.configs don't appear to be used in .NET apps.
- Whilst developers who build their apps on .NET Framework 4.7+ maybe familiar with app.config ApplicationConfigurationSection and/or app.config AppContextSwitchOverrides, we have removed all dependencies on app.config and these quirks in .NET Core 3.0 timeframe.
💭 It is also worth noting that quirks were necessary in .NET Framework, which was installed in-place, and didn't provided any kind of side-by-side deployment. In a rare and unfortunate event where we may accidentally break end users experiences in a given release users will be able to install a previous working .NET release and run their application against it until a fix becomes available.
- There may also be an argument that app.config can be changed by end users, thus allowing to alter behaviours of applications without a recompilation, making the app.config the prefered configuration vehicle. But it is important to note that our SDK is built with developers in mind, and not the ability of end users to alter the application behaviours at will. If an app developer feels it is important to allow end users to change the default font or dpi settings via an app.config it is the developers responsibility to facilitate that mechanism to end users.
Initial thinking was to allow configuration of only Font.FamilyName
and Font.Size
properties. However these two properties may be insufficient in some use cases, which otherwise would be achievable if Application.SetDefaultFont(Font)
API was invoked manually.
E.g.:
Application.SetDefaultFont(new Font(new FontFamily("Calibri"), 11f, FontStyle.Italic | FontStyle.Bold, GraphicsUnit.Point));
It would rather be impractical to provide an MSBuild properties for each argument the Font
constructor takes, and instead the proposal is to configure fonts via a single property ApplicationDefaultFont
. This property will have a format equivalent to the output of FontConverter.ConvertToInvariantString()
API e.g.:
name, size[units[, style=style1[, style2, ...]]]
It is theoretically possible that Font.FamilyName
may be locale sensitive, i.e. a different font family (and size) could be used in different regions of the world. However I was unable to find any real world scenarios or solicit user feedback. We may consider providing an implementation supporting this use case when such scenario will be made available to us.
Runtime:
- If the value of
ApplicationDefaultFont
resource can't be parsed, this will result in a compilation error. - If the font name supplied in the
ApplicationDefaultFont
value is invalid, this will result in a runtime error.
Designer:
- If the value of
ApplicationDefaultFont
resource can't be parsed, this will result in the designer process load failure. - If the font name supplied in the
ApplicationDefaultFont
value is invalid, this will result in the designer process load failure.
At this point an astute Windows Forms developer will say that there are currently 3 different ways to configure dpi settings (which may or may not be mutually exclusive or complimentary):
- app.manifest - Common Control related configuration, dealing with it was always a pain. Very likely that a significant part of Windows Forms developers have never heard of it.
- app.config ApplicationConfigurationSection and app.config AppContextSwitchOverrides.
Application.SetHighDpiMode(HighDpiMode)
.
This proposal introduces a 4th way of configuring dpi, and to make it successful it has to (paraphrasing JRR Tolkien):
One API to rule them all, One API to find them,
One API to bring them all and in the ApplicationConfiguration bind them...
The benefit the new approach provides is by facilitating the sharing of the information between the runtime and the designer during the development phase, as well as unifying how dpi settings are configured. This benefit is believed to outweigh the need to remove several lines of code from Main()
method and adding several MSBuild properties.
Generally speaking, this proposal is orthogonal to top level programs. Windows Forms applications can already be written as top level programs today, e.g.:
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using ProjectConfiguration;
// Apartment must be set to Unknown first.
Thread.CurrentThread.SetApartmentState(ApartmentState.Unknown);
Thread.CurrentThread.SetApartmentState(ApartmentState.STA);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(defaultValue: false);
Application.SetDefaultFont(new Font(new FontFamily("Arial"), 12f));
Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.Run(new Form1());
In the long term we could consider supporting a host/builder model, similar to the suggestion in dotnet/winforms#2861 and implementation in alex-oswald/WindowsFormsLifetime. E.g.:
[main: STAThread]
CreateHostBuilder().Build().Run();
public static IHostBuilder CreateHostBuilder() =>
Host.CreateDefaultBuilder(Array.Empty<string>())
.UseWindowsFormsLifetime<Form1>(options =>
{
options.HighDpiMode = HighDpiMode.SystemAware;
options.EnableVisualStyles = true;
options.CompatibleTextRenderingDefault = false;
options.SuppressStatusMessages = false;
options.EnableConsoleShutdown = true;
})
.ConfigureServices((hostContext, services) =>
{
// ...
});
However we need to be congnisant of the fact that Windows Forms apps often have elaborate application bootstrap logic (e.g. this, this, or this) that could be difficult to translate or adapt to this syntax, so this direction will require deeper investigations and feasibility studies.
Other UI SDKs (e.g. WPF and UWP) are XAML-based, and their designers read the same XAML files as their runtime, and run essentially the same app just in a different context (shell/window integration, app model). WinUI doesn't currently have a designer.