WinForms .NET Core Tips

I needed to do a big re-write on an old VB6 application used by a client of mine. I started the re-write in standard WinForms, but when the .NET Core version came to mainstream I decided to complete the re-write in WinForms .NET Core, which involved some pain, but I believed would be worth it in the end. I’ve put this post together to highlight some of the issues I found and what needs to be done by the developer.

Project Files

Settings are not included in the project by default unlike a standard WinForms project. If you need to persist user settings then include a .settings file in the Properties folder.

If you will be using logging or need other application configuration settings you should add an appsettings.json file as this is loaded by default if it exists when using CreateDefaultBuilder which you will be using if you are going to use Dependency Injection (and you really should). Remember to set the “Build Action” property for the file to “Content” and the “Copy to Output Directory” property for the file to “Copy if newer”.

Entity Framework Core

I had an existing database and I wanted to use Entity Framework Core to use the existing database and generate a model within the project. Unlike a standard WinForms project you can’t simply ass an “ADO.NET Entity Data Model”. Instead you need to use NuGet packages and issue a Scaffold-DbContext command, e.g.

PM> install-package Microsoft.EntityFrameworkCore
PM> install-package Microsoft.EntityFrameworkCore.SqlServer
PM> install-package Microsoft.EntityFrameworkCore.Tools
PM> Scaffold-DbContext "Server=.;Database=TestDatabase;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -UseDatabaseNames

Dependency Injection

Dependency Injection will allow us, if we do it right, to make the code loosely coupled to ease maintenance, more easily tested for higher quality, with smaller single responsible classes that will be easier to maintain, and easier to extend.

You will need to install this package:

PM> install-package Microsoft.Extensions.Hosting

Setting up Dependency Injection can then be achieved with code similar to this in the program.cs file (note how the main form is now obtained from the service provider):

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Windows.Forms;
using TestApp.Models;

namespace TestApp
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.SetHighDpiMode(HighDpiMode.SystemAware);
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            var host = Host.CreateDefaultBuilder()
                .ConfigureServices((context, services) =>
                {
                    ConfigureServices(context.Configuration, services, GetDatabaseConnectionString());
                })
                .Build();

            var services = host.Services;
            var mainForm = services.GetRequiredService();

            Application.Run(mainForm);
        }

        private static void ConfigureServices(IConfiguration configuration, IServiceCollection services, string connectionString)
        {
            // Entity framework database context
            services.AddDbContext
                (Options => Options.UseSqlServer(connectionString));

            // Main form
            services.AddSingleton<MainForm>();

            // Transient Services
            services.AddTransient<IBusinessLayerClass1, BusinessLayerClass1>();

            // Singleton Services
            services.AddSingleton<IBusinessLayerClass2, BusinessLayerClass2>();

            // Other forms
            services.AddTransient<ChildForm1>();
        }

        private static string GetDatabaseConnectionString()
        {
            // Get this from an encrypted config file or protect in some way if including username/passwords
            return @"data source=.;initial catalog=TestDatabase;" +
                "integrated security=True;persist security info=False;pooling=True;" +
                "multipleactiveresultsets=True;connect timeout=5;encrypt=False;" +
                "trustservercertificate=True;App=TestApp;";
        }
    }
}

Using Child Forms and Services

Now we have some services and a child form defined, we can use them within our main form, e.g.

using Microsoft.Extensions.DependencyInjection;
using System;
using System.Windows.Forms;

namespace TestApp
{
    internal partial class MainForm : Form
    {
        private readonly IServiceProvider _serviceProvider;
        private readonly IBusinessLayerClass1 _businessLayerClass1;

        public MainForm(IServiceProvider serviceProvider, IBusinessLayerClass1 businessLayerClass1)
        {
            InitializeComponent();

            _serviceProvider = serviceProvider;
            _businessLayerClass1 = businessLayerClass1;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            _businessLayerClass1.SomeTask();

            var childForm = _serviceProvider.GetRequiredService<ChildForm1>();
            childForm.Show();
        }
    }
}

DataGridView

Unfortunately, at least in Visual Studio v16.6.2 the DataGridView is not properly implemented in the Designer for WinForms .NET Core. My workaround was to use Visual Studio v16.7.0 Preview, but even then it is a bit flakey when it comes to the columns, so I would do the design of the DataGridView in the designer and then code up the columns in the form Load event. I see this as a temporary measure until such time as the Designer gets more love from Microsoft e.g.

private void SetupColumn(
    DataGridViewTextBoxColumn column,
    DataGridViewAutoSizeColumnMode autoSizeMode,
    string dataPropertyName,
    string headerText,
    int minimumWidth,
    string name,
    int width,
    bool visible
    )
{
    column.AutoSizeMode = autoSizeMode;
    column.DataPropertyName = dataPropertyName;
    column.HeaderText = headerText;
    column.MinimumWidth = minimumWidth;
    column.Name = name;
    column.ReadOnly = true;
    column.Width = width;
    column.SortMode = DataGridViewColumnSortMode.Automatic;
    column.Visible = visible;
}

//// Need to do this until Microsoft add DataGridView into .NET Core WinForms Designer properly
private void SetupDataGrid()
{
    BranchId = new DataGridViewTextBoxColumn();
    BranchCode = new DataGridViewTextBoxColumn();
    BranchName = new DataGridViewTextBoxColumn();
    Status = new DataGridViewTextBoxColumn();

    dataGrid.Columns.AddRange(new DataGridViewColumn[] {
        BranchId,
        BranchCode,
        BranchName,
        Status});

    dataGrid.ColumnHeadersDefaultCellStyle.SelectionBackColor =
        dataGrid.ColumnHeadersDefaultCellStyle.BackColor;

    SetupColumn(BranchId, DataGridViewAutoSizeColumnMode.None, "BranchId", "", 12, "BranchId", 108, false);
    SetupColumn(BranchCode, DataGridViewAutoSizeColumnMode.None, "BranchCode", "Code", 12, "BranchCode", 108, true);
    SetupColumn(BranchName, DataGridViewAutoSizeColumnMode.Fill, "BranchName", "Name", 12, "BranchName", 108, true);
    SetupColumn(Status, DataGridViewAutoSizeColumnMode.Fill, "Status", "", 12, "Status", 108, false);
}

Leave a Reply

Your email address will not be published. Required fields are marked *