Skip to content

Latest commit

 

History

History
514 lines (372 loc) · 27.4 KB

streamline-application-bootstrap.md

File metadata and controls

514 lines (372 loc) · 27.4 KB

Streamline Windows Forms application configuration and bootstrap

Owner Igor Velikorossov

Overview

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.

Proposal

The purpose of this proposal is to:

  1. streamline a Windows Forms application configuration and bootstrap,
  2. 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.

Scenarios and User Experience

Refer to Design section below.

  • Runtime:
    projectconfiguration

  • Design time
    projectconfiguration

Requirements

Goals

  1. 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

  2. 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.

  3. 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.
  4. 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.

  5. The Windows Forms application template must be updated with the new bootstrap experiece.

Stretch Goals

  1. Update Visual Studio property page for Windows Forms projects.

  2. Build the following Roslyn Analyzers functions:

    • Check for invocations of now-redundant Applicaiton.* methods invoked by ApplicationConfiguration.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 than HighDpiMode.SystemAware (see: dotnet/winforms-designer#3278)
  3. Update the Windows Forms application template using the top level statements syntax.

Non-Goals

  1. Provide source generator/analyzer support for apps built on .NET Framework, .NET Core 3.1, or .NET 5.0.

  2. Design/implement top level statements fo Windows Forms applications. Tracked under: dotnet/winforms#5074

  3. Design/implement new host/builder model for Windows Forms applications.

  4. Migrate Visual Basic apps off the Application Framework or change the use of *.myapp file.

  5. Streamline the bootstrap of Visual Basic apps or use Roslyn source generators for this purpose.

  6. Use Roslyn analyzers in Visual Basic scenarios until previous two items are addressed.

  7. Provide a mechanism to configure locale aware font descriptors.

  8. 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)

Stakeholders and Reviewers

  • 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

Design

1. Visual Basic

Ironically Visual Basic apps are in a good position for the designer integration with their Application Framework functionality, i.e. "*.myapp" file:

image

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.

2. C#

Affected API

    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());
       }
    }

MSBuild properties

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>

Implementation details

  1. The new bootstrap API must only work for Windows applications projects (i.e. OutputType = WinExe and OutputType = 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:
    image

  2. 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());
           }
        }
  3. 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.

3. Visual Studio: designer

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.

4. Visual Studio: project system

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:

image

Tracked under dotnet/project-system#7279

Q & A

Why store settings as MSBuild properties (e.g. in csproj or props files)?

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.

Font Configuration

Default scenario

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, ...]]]

Locale aware font configuration

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.

Error scenarios

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.

How to resolve dpi settings?

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):

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.

What about top level programs and host/builder model?

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.

Does this approach align/conflict with other UI SDKs, e.g. WPF or WinUI?

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.