dotnet / project-system

The .NET Project System for Visual Studio
MIT License
968 stars 386 forks source link

Conditional Compilation Symbols GUI (preprocessor symbols) #6769

Open verelpode opened 3 years ago

verelpode commented 3 years ago

Visual Studio Version: 16.8.2

Summary

Visual Studio is a truly wonderful product, but some aspects of it cause hassles every week on average. Currently the GUI for Conditional Compilation Symbols in Project Properties window in Visual Studio (16.8.2) is inadequate and a hassle to use, and inconsistent with other parts of Visual Studio. It fails to support large projects that need a larger quantity of symbols. It is unfortunately only a textbox (and narrow), despite the fact that it actually represents a list of values. The current textbox suggests that only a single value should be entered, but in reality it's a case of a textbox being misused as a substitute for a list GUI. Screenshot:

image

To solve this problem, I suggest that the symbols textbox be replaced with a list GUI like the following mockup for C# projects:

image

For comparison, see also the preexisting list GUI for "Environment variables:" in the "Debug" panel in Project Properties of an ASP.NET Core project:

image

C# versus C++ Differences/Considerations

In C#, a symbol can only be enabled or disabled (boolean), whereas in C++, a value can be assigned to a symbol. I began my career using C++ but I mostly stopped using C++ long ago because I noticed that my productivity was much better when I used C#, thus the GUI for C++ projects is unimportant for me, but anyway, for people who still use a lot of C++ code, remember that C++ symbols allow a value to be assigned to each symbol. Thus the GUI for C++ symbols should be like the above "Environment variables" screenshot, whereas the GUI for C# symbols can be a list of tickboxes/checkboxes. Here is a mockup for C++:

image

Alternatively, you may wish to use the same symbol list GUI for both C# and C++ projects. This is also a good idea. Thus the symbol list would look like the above two-column mockup regardless of whether it is for a C# or C++ project. However, if it is a C# project, Visual Studio would limit the user to entering only a boolean true or false value for each symbol (not an integer value). If it is a C++ project, Visual Studio would allow the symbol value to be true or false or an integer.

Example C++ Code Value in new GUI in VS
#define EXPERIMENTAL true
#undef EXPERIMENTAL (or otherwise not defined) false
#define XYZ_VERSION 5001 5001

Re Multiple Configurations in a project

Regarding multiple Configurations in a .csproj (such as the "Debug" and "Release" Configurations), the list/names of Conditional Compilation Symbols should be the same for all Configurations, except that the tick/value of the symbols should be different for each Configuration. For example, the symbols DEBUG and TRACE should appear in the symbol list regardless of whether the current Configuration is "Debug" or "Release", but when the "Release" configuration is chosen, the DEBUG symbol would be shown as unticked or Value=false. Thus when you use the GUI to view a different configuration, only the tick or value of each symbol should change, not the symbol names nor the quantity of symbols.

Alternatively, if compatibility reasons or other reasons lead to a final decision to store a completely separate symbol list for each Configuration, then this would still be OK provided it has the ability to enable and disable each symbol without deleting symbols from the list.

Steps to Reproduce

  1. Open the Project Properties window of a project.
  2. Click "Build" on the left.
  3. Look for the textbox "Conditional compilation symbols".
  4. Notice how awkward this textbox is, especially when it contains more symbols than fit in such a narrow textbox.
  5. Notice how VS does not provide any way to disable a symbol without deleting it from the textbox.

Expected Behavior

Actual Current Behavior

User Impact

tmeschter commented 3 years ago

@drewnoakes There's a lot to look at here, but we should consider this feedback as we work on the new property pages.

verelpode commented 3 years ago

@tmeschter -- Thanks for considering it. Here's some details about the storage aspect:

Storage of the symbols in .csproj files

To store what the suggested new symbols GUI needs, here is an example of a backwards-compatible extension to the .csproj format:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>net5.0</TargetFramework>

        <ConditionalCompilationSymbols>
            <!-- This section determines the symbol names that should appear in the symbol list in the VS GUI, but NOT the value or tick status of each symbol. -->
            <ConditionalCompilationSymbol Name="DEBUG" Comment="" />
            <ConditionalCompilationSymbol Name="TRACE" Comment="" />
            <ConditionalCompilationSymbol Name="EXPERIMENTAL" Comment="The program might crash if you enable this symbol." />
            <ConditionalCompilationSymbol Name="UNFINISHED" Comment="The project might not compile if you enable this symbol." />
            <ConditionalCompilationSymbol Name="TEMP_TESTING" Comment="Never enable this in the Release config!" />
            <ConditionalCompilationSymbol Name="EXAMPLE_SYM_1" Comment="Blah blah blah." />
            <ConditionalCompilationSymbol Name="EXAMPLE_SYM_2" Comment="Blah blah." />
            <ConditionalCompilationSymbol Name="EXAMPLE_SYM_3" Comment="" />
        </ConditionalCompilationSymbols>

        <!-- If desired, the following could potentially be supported to instruct the GUI to use symbols from shared symbol files used in multiple .csproj files: -->
        <ConditionalCompilationSymbols Include="..\Unity\Unity-Symbols.xml" />
        <ConditionalCompilationSymbols Include="..\ExampleFolder\Our-Common-Symbols.xml" />

    </PropertyGroup>

    <!-- The following format is unchanged (the same format as currently used in .csproj files). -->
    <!-- The following determines which symbols are ticked (or Value=true) in the VS GUI. -->
    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
        <DefineConstants>DEBUG; TRACE; EXPERIMENTAL; EXAMPLE_SYM_2</DefineConstants>
    </PropertyGroup>

    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
        <DefineConstants>TRACE; EXAMPLE_SYM_2</DefineConstants>
    </PropertyGroup>

</Project>

If desired, the above also includes an example of a potential feature that could allow symbol names to be loaded from separate XML files shared between multiple projects, in addition to storing symbol names directly inside each .csproj file. For example, where it says Unity-Symbols.xml above, this file could contain a list of the symbols described in the "Platform dependent compilation" page of the Unity3D manual.

Thus the file Unity-Symbols.xml could contain:

<ConditionalCompilationSymbols>
    <ConditionalCompilationSymbol Name="UNITY_EDITOR" Comment="directive to call Unity Editor scripts from your game code." />
    <ConditionalCompilationSymbol Name="UNITY_EDITOR_WIN" Comment="directive for Editor code on Windows." />
    <ConditionalCompilationSymbol Name="UNITY_EDITOR_OSX" Comment="directive for Editor code on Mac OS X." />
    <!-- ... and so forth ... -->
</ConditionalCompilationSymbols>

Note the shared Unity-Symbols.xml file would NOT store the value/boolean/tick/integer of each symbol, rather it should only store the name of each symbol (and optionally a comment/description). The value/tick of each symbol remains stored in the same place as it is currently stored (the <DefineConstants> inside each .csproj file).

Unity3D is just a good common example although I don't use it myself. In my case, I'd still be happy if the final decision is to skip the extended idea of loading symbols from separate/shared files. Instead of a shared file, I could copy-and-paste the same <ConditionalCompilationSymbols> section to multiple .csproj files, because the symbol values/ticks are stored outside of the <ConditionalCompilationSymbols> section. Thus it is beneficial to store the full list of symbol names in a separate part of the .csproj file than where the symbol values(ticks) are stored (<DefineConstants>).

For reference and terminology, see also: "#define (C# Reference)"

jjmew commented 3 years ago

@mckennabarlow This is something that we should investigate and see what design we will want to do here.

verelpode commented 3 years ago

If the additional idea of shared symbol files will also be supported, then here is a potential way of doing it using the preexisting <Import> element in MSBuild. Firstly, insert one or more <Import> elements into a ".csproj" file like this:

<Import Project="..\ExampleFolder\Unity-Symbols.props" />

And the "Unity-Symbols.props" file would contain, for example:

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>

        <ConditionalCompilationSymbols>
            <!-- This section determines the symbol names that should appear in the symbol list in the VS GUI, but NOT the value or tick status of each symbol. -->
            <ConditionalCompilationSymbol Name="UNITY_EDITOR" Comment="directive to call Unity Editor scripts from your game code." />
            <ConditionalCompilationSymbol Name="UNITY_EDITOR_WIN" Comment="directive for Editor code on Windows." />
            <ConditionalCompilationSymbol Name="UNITY_EDITOR_OSX" Comment="directive for Editor code on Mac OS X." />
            <!-- ... and so forth ... -->
        </ConditionalCompilationSymbols>

    </PropertyGroup>
</Project>

That's all preexisting stuff except for the <ConditionalCompilationSymbols> and <ConditionalCompilationSymbol> elements.

Thus in the same manner as how MSBuild already operates, the new <ConditionalCompilationSymbols> element could be used in the .csproj file AND in any imported .props files. According to MSBuild, as I understand it, any properties such as the suggested <ConditionalCompilationSymbols> must be inside an MSBuild <PropertyGroup> element regardless of whether it is in a .props or .csproj file.

Importantly, the contents of the effective/final symbol list (displayed in VS GUI) should not be obliterated by any subsequent or imported <ConditionalCompilationSymbols> elements, rather they should be concatenated. Thus if both the .csproj and one or more imported .props files contain <ConditionalCompilationSymbols>, then the symbols in the .props file(s) should supplement not replace the symbols in the .csproj file.

Re the preexisting <DefineConstants> property/element, the behavior of <DefineConstants> would remain unchanged, thus backwards-compatibility is preserved, but luckily this isn't any sacrifice for compatibillity reasons because in any event it's beneficial to store the symbol values/ticks in a different place/element than where the full list of symbol names will be stored.

By the way, currently, any second or third <DefineConstants> does obliterate/replace all prior <DefineConstants> elements, and this behavior could remain as-is, but the suggested new <ConditionalCompilationSymbols> should supplement-not-obliterate any prior <ConditionalCompilationSymbols> elements.

tmeschter commented 3 years ago

@verelpode I have a couple of questions, and the answers will help us better understand your needs.

  1. Do you frequently change the conditional compilation symbols?
  2. If so, why do you change them?

The current UI is based on the assumption that the number of conditional compilation symbols is generally small, and rarely changed.

verelpode commented 3 years ago

@tmeschter wrote:

"The current UI is based on the assumption that the number of conditional compilation symbols is generally small, and rarely changed."

I disagree that the current UI assumes that the symbols are rarely changed. The current UI has a tickbox "Define TRACE constant" -- this tickbox implies that TRACE is frequently changed (or at least more than rarely changed).

image

In my opinion, the current UI's attitude toward frequent changes is a self-contradictory "Yes but also No" attitude. The TRACE tickbox implies frequent changes but the symbols textbox implies rare changes, thus it's currently self-contradictory.

I'll answer your question anyway. In my opinion, the assumption you mentioned was only valid in the past, or only valid with small simple projects, or perhaps it was never valid even for small projects (considering TRACE). For example, the Unity3D manual lists 40 symbols, and the MS documentation page "#if (C# reference)" lists 33 symbols (an incomplete list; more than 33 MS symbols exist), and other projects likewise involve 10-40 symbols, and my work also uses approx 20 symbols, yet the current UI is only practical for approx 3 symbols.

An example: The EXPERIMENTAL symbol

Re frequency of changes, for example in my previous screenshots I made a symbol named EXPERIMENTAL, and I realize people might suggest that I create a new Configuration named "Experimental" that defines the symbol EXPERIMENTAL, and I realize that I could then use the preexisting Config chooser control to frequently and easily switch between Configs named "Experimental", "Debug", and "Release", but this technique is broken. Should a new Config named "Experimental" be configured with settings suitable for debugging or for release? In order to test the symbol EXPERIMENTAL with both debug and release settings, I'd be forced to create 4 Configurations:

Config Name Symbols Defined
Debug DEBUG; TRACE;
Release TRACE;
Experimental Debug EXPERIMENTAL; DEBUG; TRACE;
Experimental Release EXPERIMENTAL; TRACE;

But now, for example, I also need to test EXPERIMENTAL with and without WebGL -- the UNITY_WEBGL symbol described in the Unity3D manual (actually I don't use Unity3D in real life, rather I have a bunch of other symbols, but I'll use Unity3D as an example). So now I'm forced to create 8 Configs:

Config Name Symbols Defined
Debug DEBUG; TRACE;
Release TRACE;
Experimental Debug EXPERIMENTAL; DEBUG; TRACE;
Experimental Release EXPERIMENTAL; TRACE;
Debug with WebGL UNITY_WEBGL; DEBUG; TRACE;
Release with WebGL UNITY_WEBGL; TRACE;
Experimental Debug with WebGL UNITY_WEBGL; EXPERIMENTAL; DEBUG; TRACE;
Experimental Release with WebGL UNITY_WEBGL; EXPERIMENTAL; TRACE;

But wait, I also need to test the program with and without the IL2CPP-based script-compiler -- the ENABLE_IL2CPP symbol also described in the Unity3D manual. So now I'm forced to create 16 Configs -- this is highly unmanageable and messy:

Config Name Symbols Defined
Debug DEBUG; TRACE;
Release TRACE;
Experimental Debug EXPERIMENTAL; DEBUG; TRACE;
Experimental Release EXPERIMENTAL; TRACE;
Debug with WebGL UNITY_WEBGL; DEBUG; TRACE;
Release with WebGL UNITY_WEBGL; TRACE;
Experimental Debug with WebGL UNITY_WEBGL; EXPERIMENTAL; DEBUG; TRACE;
Experimental Release with WebGL UNITY_WEBGL; EXPERIMENTAL; TRACE;
Debug with IL2CPP ENABLE_IL2CPP; DEBUG; TRACE;
Release with IL2CPP ENABLE_IL2CPP; TRACE;
Experimental Debug and IL2CPP ENABLE_IL2CPP; EXPERIMENTAL; DEBUG; TRACE;
Experimental Release and IL2CPP ENABLE_IL2CPP; EXPERIMENTAL; TRACE;
Debug with WebGL and IL2CPP ENABLE_IL2CPP; UNITY_WEBGL; DEBUG; TRACE;
Release with WebGL and IL2CPP ENABLE_IL2CPP; UNITY_WEBGL; TRACE;
Experimental Debug with WebGL and IL2CPP ENABLE_IL2CPP; UNITY_WEBGL; EXPERIMENTAL; DEBUG; TRACE;
Experimental Release with WebGL and IL2CPP ENABLE_IL2CPP; UNITY_WEBGL; EXPERIMENTAL; TRACE;

Add one more symbol and you need 32 Configs.

But wait, I work with multiple .csproj's in real life. I need these 32 Config's in all of the .csproj's. So I'd be forced to spend hours recreating 32 Config's in multiple different .csproj's that all need the same list of 32 Config's (alternatively I could manually edit the XML inside each .csproj file and hope I don't make any mistakes).

See, that doesn't work. What's needed is a listbox of symbols with tickboxes or a "Value" column, so each symbol can be individually enabled or disabled independently of other symbols, frequently and easily. Even better would be if the symbol names list (without values) could be shared between multiple .csproj's (such as via the preexisting MSBuild <Import> element).

Another example: The UNFINISHED symbol

In my previous screenshots I also made a symbol named UNFINISHED. Often I need to compile and run (with or without VS debugger) the very latest dev-build of the program, but it cannot be compiled because of some unfinished new classes or methods. So I wrap the unfinished portions with #if UNFINISHED and undefine the UNFINISHED symbol, and then the app successfully compiles and runs. Later the same day, I need to redefine the UNFINISHED symbol in order to resume work on the unfinished code sections. Thus frequent changes occur to the enable/disable status of the UNFINISHED symbol.

Class Libraries (or Nuget Packages) versus Apps

I believe it's also important to consider the difference in usage characteristics of class libraries (or nuget packages) versus apps. Developers who develop class libraries and/or nuget packages may well need to change the symbols more frequently than app developers, in order to support multiple variations of the same library or nuget package.

Before publishing a new version of a nuget package, the developer(s) want to test that every variation still works correctly, and this means enabling and disabling symbols each time a new version of the library will be published, i.e. frequently. Again using Unity3D as an example, Unity3D is a class library, so the Unity3D developers would want to frequently enable and disable various combinations of the following symbols, to test Unity3D prior to publishing each new version:

Conditionally-Compiled Feature Symbol Name
Whether WebGL is supported UNITY_WEBGL
Whether Analytics are performed UNITY_ANALYTICS
Whether the IL2CPP script-compiler is supported ENABLE_IL2CPP
Whether "Unity Editor scripts" scripts are supported UNITY_EDITOR
Unity library for .NET Standard NET_STANDARD_2_0
Unity library for .NET Framework 4.6 NET_4_6
Unity library for .NET Legacy NET_LEGACY
etc etc
etc etc
etc etc
Whether the UI is excluded (for Console and Server programs) ...
Whether the UI is compiled for WPF ...
Whether the UI is compiled for WinUI 3 ...
Whether the UI is compiled for UWP/WinRT ...
Whether the UI is compiled for Xamarin UI ...
Whether the UI is compiled for Mono-WinForms ...