2

How can I force a Dotnet6 Desktop Winforms App to use appsettings.json instead of Settings.settings[App.config]?

If I create a new desktop application in Visual Studio 2022 to use Dotnet6.0, it, by default will create a Settings.settings file on the fly, then create an App.config file as well (which is odd as this was supposedly deprecated in Dotnet5.0 and up!).

Unfortunately, all settings are embedded in the application after installation/publishing and cannot be changed anywhere in the application folder, once installed.

For older DotNet4.72 desktop applications, the app.config was always in the release output and, if changed, the app would read any changes just fine upon execution.

I'd like to use the appsettings.json just like for web apps (dotnet6.0) and there's a lot of info on how to do this with console apps (dotnet6.0), but not for desktop windows.

I have quite a few DotNet4.72 Desktop Apps that I wish to convert that still require the ability to simply change a configuration setting by editing a file on the fly (if conditions change). While I can roll my own solution, I'd like to do this similarly like in my web applications (appsettings.json). Better yet, I'd love to see a VS2022 desktop template that does this every time a new desktop app is created for DotNet6 and 7.

Any ideas?

Jeremy Thompson
  • 61,933
  • 36
  • 195
  • 321
MC9000
  • 2,076
  • 7
  • 45
  • 80
  • According to this [article](https://learn.microsoft.com/en-us/ef/core/miscellaneous/connection-strings#winforms--wpf-applications): _WinForms, WPF, and ASP.NET 4 applications have a tried and tested connection string pattern. The connection string should be added to your application's App.config file (Web.config if you are using ASP.NET)_ – Tu deschizi eu inchid Dec 14 '22 at 22:55
  • The following may be helpful: https://stackoverflow.com/a/74576869/10024425 – Tu deschizi eu inchid Dec 14 '22 at 22:56
  • 2
    There's no difference in the initialization of the IHost default Builder. You do the same thing in a Console or WinForms app. Since you have found some posts about it, avoid all those that explicitly hardcode `appsetting.json` (e.g., `.AddJsonFile("appsettings.json");`). There's no need for that (it's actually an anti-pattern). Add `appsetting.json` to the Project, then execute the Default Builder, it will load the settings on its own. Also, you may have seen something like `[IHost instance].Run()`: don't do that, since of course your app won't run anymore (without Dependency Injection) – Jimi Dec 14 '22 at 23:09
  • 1
    You won't find a standard WinForms Template that automatically adds `appsettings.json`, this platform uses a different paradigm in relation to Settings (which includes Application Settings Binding - even though in .NET 5+ the previous interface is not yet available). You have to create all bindings manually, there's no *native* handler for this. But, since you can specify what Service (can be a simple class) handles the settings, in `[IHost].Services.GetService>();`, you simply implement `INotifyPropertyChanged` in your class handler – Jimi Dec 14 '22 at 23:23
  • [Using .NET 5, .NET 6 or .NET Core configuration system in Windows Forms](https://stackoverflow.com/a/65675178/3110834) – Reza Aghaei Jul 20 '23 at 21:09

1 Answers1

8

Basically you can use the Microsoft.Extensions.Configuration but you can also make use of Microsoft.Extensions.Hosting nuget package as well.

Here is a tested working example that handles configuration change as well.

Steps:

  • Add an appsettings.json file to the project.
    • Set Build Action to None.
    • Set Copy to Output Directory to Copy always.
  • Create a class for your options. (In this example: SampleOptions)
  • Configure your options in appsettings.json.
  • Add Microsoft.Extensions.Hosting nuget package to your project.
  • Modify Program.cs:
    • Configure and build a host. (e.g. register your forms, options into DI)
    • Start host.
    • Resolve IHostApplicationLifetime.
    • Create an IServiceScope using host's IServiceProvider.
    • Resolve your MainForm.
    • Call Application.Run() method with your resolved MainForm instance.
  • Modify MainForm.cs:
    • Inject IOptionsMonitor<SampleOptions> into the .ctor
    • Register a listener to be called whenever SampleOptions changes
      using IOptionsMonitor<SampleOptions>.OnChange method.

Here are the files.

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  },
  "SampleOptions": {
    "SampleStringKey": "Sample value",
    "SampleIntegerKey": 42
  }
}

SampleOptions.cs

namespace WinFormsAppNet6;

public class SampleOptions
{
    public string SampleStringKey { get; set; } = "N/A";

    public int SampleIntegerKey { get; set; }
}

Program.cs

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace WinFormsAppNet6;

internal static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static async Task Main() // <-- 'async Task' instead of 'void'
    {
        // To customize application configuration such as
        // set high DPI settings or default font,
        // see https://aka.ms/applicationconfiguration.
        ApplicationConfiguration.Initialize();

        using IHost host = CreateHost();
        await host.StartAsync();

        IHostApplicationLifetime lifetime = 
            host.Services.GetRequiredService<IHostApplicationLifetime>();

        using (IServiceScope scope = host.Services.CreateScope())
        {
            var mainForm = scope.ServiceProvider.GetRequiredService<MainForm>();

            Application.Run(mainForm);
        }

        lifetime.StopApplication();
        await host.WaitForShutdownAsync();
    }

    private static IHost CreateHost()
    {
        string[] args = Environment.GetCommandLineArgs().Skip(1).ToArray();

        HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

        builder.Services.AddSingleton<MainForm>();

        builder.Services.Configure<SampleOptions>(
            builder.Configuration.GetSection(nameof(SampleOptions))
        );

        return builder.Build();
    }
}

MainForm.cs

Controls:

  • Button: UpdateConfigurationButton
  • Button: ExitButton
  • RichTextBox: AppLog
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Windows.Forms;

using Microsoft.Extensions.Options;

namespace WinFormsAppNet6;

public partial class MainForm : Form
{
    private readonly IOptionsMonitor<SampleOptions> optionsMonitor;

    private readonly JsonSerializerOptions jsonSerializerOptions =
        new(JsonSerializerDefaults.Web)
        {
            WriteIndented = true
        };

    public MainForm(IOptionsMonitor<SampleOptions> optionsMonitor)
    {
        InitializeComponent();

        this.optionsMonitor = optionsMonitor;
        optionsMonitor.OnChange(OnOptionsChange);

        LogOptions(optionsMonitor.CurrentValue);
    }

    private void OnOptionsChange(SampleOptions options)
    {
        LogOptions(options);
    }

    private void LogOptions(
        SampleOptions options,
        [CallerMemberName] string? callerMemberName = null
    )
    {
        AppendLog(
            Environment.NewLine + JsonSerializer.Serialize(options),
            callerMemberName
        );
    }

    private void AppendLog(
        string message,
        [CallerMemberName] string? callerMemberName = null
    )
    {
        if (AppLog.InvokeRequired)
        {
            AppLog.Invoke(() => AppendLog(message, callerMemberName));
            return;
        }

        AppLog.AppendText(
            $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.ffffff} [{nameof(MainForm)}]::[{callerMemberName ?? "Unknown"}] {message}{Environment.NewLine}"
        );
    }

    private void UpdateConfigurationButton_Click(object sender, EventArgs e)
    {
        const string AppSettingsJsonFileName = "appsettings.json";

        if (!File.Exists(AppSettingsJsonFileName))
        {
            AppendLog(
                $"{nameof(FileNotFoundException)}: {AppSettingsJsonFileName}"
            );

            return;
        }

        string jsonContent = File.ReadAllText(AppSettingsJsonFileName);
        JsonNode? rootNode = JsonNode.Parse(jsonContent);

        if (rootNode is null)
        {
            AppendLog($"{nameof(JsonException)}: File parse failed.");
            return;
        }

        AppendLog(
            $"Finding key: {nameof(SampleOptions)}:{nameof(SampleOptions.SampleStringKey)}"
        );

        JsonObject rootObject = rootNode.AsObject();
        JsonObject? optionsObject = rootObject[nameof(SampleOptions)]?.AsObject();

        if (optionsObject is null)
        {
            AppendLog(
                $"{nameof(KeyNotFoundException)}: {nameof(SampleOptions)}:{nameof(SampleOptions.SampleStringKey)}"
            );

            return;
        }

        AppendLog(
            $"Updating key: {nameof(SampleOptions)}:{nameof(SampleOptions.SampleStringKey)}"
        );

        optionsObject[nameof(SampleOptions.SampleStringKey)] =
            JsonValue.Create(
                $"Value modified at {DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.ffffff}"
            );

        AppendLog($"Saving file: {AppSettingsJsonFileName}");

        using var stream =
            new FileStream(
                AppSettingsJsonFileName,
                FileMode.OpenOrCreate,
                FileAccess.Write,
                FileShare.None
            );

        jsonContent = rootObject.ToJsonString(jsonSerializerOptions);

        stream.Write(Encoding.UTF8.GetBytes(jsonContent));
        stream.Flush();
    }

    private void ExitButton_Click(object? sender, EventArgs e)
    {
        Application.Exit();
    }
}

Screenshot Running the sample application

Gabor
  • 3,021
  • 1
  • 11
  • 20