Introduction

An exploration of the singleton pattern for C# Windows Form development which when understood and used properly provides a single instance of a class which can be used across actions in an application. Unlike the majority of threads on the web which explain the pros and cons for singleton classes with no real world code samples this article presents code samples that hit on common operations e.g. working with database connection strings, retrieving images from project resource or remembering folder locations that don't change.

There will be both practical and impractical examples for what works, does not work and should be avoided.

The singleton pattern is one of the best-known patterns in software engineering. Essentially, a singleton is a class which only allows a single instance of itself to be created, and usually gives simple access to that instance. Most commonly, singletons don't allow any parameters to be specified when creating the instance - as otherwise a second request for an instance but with a different parameter could be problematic! (If the same instance should be accessed for all requests with the same parameter, the factory pattern is more appropriate.) This article deals only with the situation where no parameters are required. Typically a requirement of singletons is that they are created lazily - i.e. that the instance isn't created until it is first needed.

Jon Skeet
https://csharpindepth.com/articles/singleton

Singleton usage is commonly called an anti-pattern were reasons range from violating the Single responsibility principle, persistent storage such as Databases, Files etc to simply not looking for other best practice patterns to work with information in an application. Simply performing a web search will return millions results were in many cases threads have been closed because they are considered opinion-based rather than fact based.
 

Points that define using Singleton in C#

  • Cannot be instantiated (private constructor instead of public)
  • Cannot be inherited (sealed class)
  • Public static instance has to be available across the whole application

Data connections explorations

For years a segment of (usually novice) developers working with databases have been creating a single connection for accessing data, open the connection, do some work then close the connection or will open a connection, perform work then leave the connection open and in each case must check if the connection is open, if not open it. 

Always close connections as soon as you are done with them, so they underlying database connection can go back into the pool and be available for other callers. Connection pooling is pretty well optimised, so there's no noticeable penalty for doing so. The advice is basically the same as for transactions - keep them short and close when you're done.

Bad pattern 1 (non-Singleton)

In this case the idea is to have one connection object for a data class.

public static void Example(string statement, SqlConnection connection)
{
    var openConn = (connection.State == ConnectionState.Open);
    if (!openConn)
    {
        connection.Open();
    }
 
    // ....
 
    if (openConn)
    {
        connection.Close();
    }
}

Bad pattern 2 (Singleton)

In this case the developer has several data classes and wants a single connection for all the data classes. Here, instantiation is triggered by the first reference to the static property Instance.

Full source for SqlServerConnections class.
public sealed class SqlServerConnections
{
    private static readonly Lazy<SqlServerConnections>
        Lazy = new Lazy<SqlServerConnections>(() => new SqlServerConnections());
 
    public static SqlServerConnections Instance => Lazy.Value;

Here is an example of calling on a connection, in this case the developer will be closing the connection once exiting the method as a using statement will close the connection no different than calling the Dispose method of the connection object.

protected DataTable ConnectionWithUsingStatement()
{
    var dt = new DataTable();
 
    using (var cn = SqlServerConnections.Instance.Connection(ConnectionString))
    {
        mHasException = false;
 
        const string selectStatement =
            "SELECT cust.CustomerIdentifier,cust.CompanyName,cust.ContactName,ct.ContactTitle, " +
            "cust.[Address] AS street,cust.City,ISNULL(cust.PostalCode,''),cust.Country,cust.Phone, " +
            "cust.ContactTypeIdentifier FROM dbo.Customers AS cust " +
            "INNER JOIN ContactType AS ct ON cust.ContactTypeIdentifier = ct.ContactTypeIdentifier;";
 
        using (var cmd = new SqlCommand() { Connection = cn, CommandText = selectStatement })
        {
            try
            {
                cn.Open();
                dt.Load(cmd.ExecuteReader());
            }
            catch (Exception e)
            {
                mHasException = true;
                mLastException = e;
            }
        }
 
    }
 
    return dt;
}

Good (non singleton) pattern

A better method to handle connections is without a singleton, instead for each method which accesses a database create a connection followed by a command object utilizing using statements so once the using statement has finished for a connection the connection is closed. Entity Framework opens and closes the connections automatically before/after an operation is completed which is what happens with a using statement for a connection.

In the following example the connection string is composed in a NuGet package class which handles the proper syntax, only the server name and default catalog are taken from the project's application configuration file.

Full source for DataOperations

The constructor for DataOperations were the first two lines read server name and default catalog from app.config for the method ConnectionUsingAppSettings. MainConnectionString is a string property which reads a full connection string from application configuration file inserted using project properties, Settings tab.
public DataOperations()
{
    DatabaseServer = ConfigurationManager.AppSettings["DatabaseServer"];
    DefaultCatalog = ConfigurationManager.AppSettings["DatabaseCatalog"];
    MainConnectionString = Settings.Default.MainConnectionString;
}

Here a connection is created, used and disposed once the operation has completed.

public DataTable ConnectionUsingAppSettings()
{
    var dt = new DataTable();
 
    using (var cn = new SqlConnection() {ConnectionString = ConnectionString})
    {
        mHasException = false;
 
        const string selectStatement =
            "SELECT cust.CustomerIdentifier,cust.CompanyName,cust.ContactName,ct.ContactTitle, " +
            "cust.[Address] AS street,cust.City,ISNULL(cust.PostalCode,''),cust.Country,cust.Phone, " +
            "cust.ContactTypeIdentifier FROM dbo.Customers AS cust " +
            "INNER JOIN ContactType AS ct ON cust.ContactTypeIdentifier = ct.ContactTypeIdentifier;";
 
        using (var cmd = new SqlCommand() { Connection = cn, CommandText = selectStatement })
        {
            try
            {
                cn.Open();
                dt.Load(cmd.ExecuteReader());
            }
            catch (Exception e)
            {
                mHasException = true;
                mLastException = e;
            }
        }
 
    }
 
    return dt;
}

Application configuration file

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
    </configSections>
    <connectionStrings>
        <add name="ApplicationDataConnectorRecommended.Properties.Settings.MainConnectionString"
            connectionString="Data Source=KARENS-PC;Initial Catalog=NorthWindAzure;Integrated Security=True" />
    </connectionStrings>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
    </startup>
  <appSettings>
    <add key="DatabaseServer" value="KARENS-PC"/>
    <add key="DatabaseCatalog" value="NorthWindAzure"/>
  </appSettings>
</configuration>

Using the full connection string from application configuration file which as the example above disposes the connection when finished.

public DataTable ConnectionUsingProjectSetting()
{
    var dt = new DataTable();
 
    using (var cn = new SqlConnection() { ConnectionString = MainConnectionString })
    {
        mHasException = false;
 
        const string selectStatement =
            "SELECT cust.CustomerIdentifier,cust.CompanyName,cust.ContactName,ct.ContactTitle, " +
            "cust.[Address] AS street,cust.City,ISNULL(cust.PostalCode,''),cust.Country,cust.Phone, " +
            "cust.ContactTypeIdentifier FROM dbo.Customers AS cust " +
            "INNER JOIN ContactType AS ct ON cust.ContactTypeIdentifier = ct.ContactTypeIdentifier;";
 
        using (var cmd = new SqlCommand() { Connection = cn, CommandText = selectStatement })
        {
            try
            {
                cn.Open();
                dt.Load(cmd.ExecuteReader());
            }
            catch (Exception e)
            {
                mHasException = true;
                mLastException = e;
            }
        }
 
    }
 
    return dt;
}

Conclusion

Using a singleton for connecting to a database is unwise as this can create issues attempting to learn why a connection is not working, especially across many classes.

Reading project resources

One method to carry images (along with other types) in an application is to store images in the project resources then when needed use code to access the stored images as shown below which sets the application Icon to a specific Icon in the project's resources.

Icon = Properties.Resources.coffeecup;

If users want the ability to select icons from a collection of icons a singleton may be used as the images don't change unless an update is performed and images then may change. The following example icons are stored in the project resources and extracted using reflection in a singleton class.

Version 1 DataTable container

In the following singleton class there are two methods to retrieve icon and bitmap images into separate DataTable containers, IconDataTable method checks to see if the DataTable to store images is null, if null retrieve images from the project resources, the same goes for bitmap images from BitMapDataTable.

using System;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Reflection;
using ProjectResources.Properties;
 
namespace ProjectResources.Classes
{
    public sealed class ResourceImages : ImageUtility
    {
        private DataTable _bitMapImagesTable;
        private DataTable _iconImagesTable;
 
        private static readonly Lazy<ResourceImages> Lazy =
            new Lazy<ResourceImages>(() => new ResourceImages());
 
        public static ResourceImages Instance => Lazy.Value;
 
        public DataTable BitMapDataTable()
        {
            if (_bitMapImagesTable == null)
            {
                GetBitMapImages();
            }
 
            return _bitMapImagesTable;
 
        }
 
        /// <summary>
        /// Container for icon resources
        /// </summary>
        /// <returns></returns>
        public DataTable IconDataTable()
        {
            if (_iconImagesTable == null)
            {
                GetIconImages();
            }
 
            return _iconImagesTable;
 
        }
 
        /// <summary>
        /// Retrieve images from project resources
        /// </summary>
        /// <remarks></remarks>
        private void GetBitMapImages()
        {
            _bitMapImagesTable = new DataTable();
 
            _bitMapImagesTable.Columns.AddRange(new[]
                {
                    new DataColumn("Identifier", typeof(int)),
                    new DataColumn("Name", typeof(string)),
                    new DataColumn("Image", typeof(Image))
                });
 
            _bitMapImagesTable.Columns["Identifier"].AutoIncrement = true;
            _bitMapImagesTable.Columns["Identifier"].AutoIncrementSeed = 1;
 
            var properties = typeof(Resources)
                .GetProperties(
                    BindingFlags.NonPublic |
                    BindingFlags.Instance |
                    BindingFlags.Static);
 
            var bitMaps = (
                from T in properties
                where T.PropertyType == typeof(Bitmap)
                select T).ToList();
 
            if (bitMaps.Count <= 0) return;
 
            foreach (var propertyInfo in bitMaps)
            {                  
                _bitMapImagesTable.Rows.Add(null,
                    propertyInfo.Name.Replace("_", " "),
                    Resources.ResourceManager.GetObject(propertyInfo.Name));
            }
        }
 
        /// <summary>
        /// Get all Icon images from project resources
        /// </summary>
        private void GetIconImages()
        {
 
            _iconImagesTable = new DataTable();
 
            _iconImagesTable.Columns.AddRange(new[]
                {
                    new DataColumn("Identifier", typeof(int)),
                    new DataColumn("Name", typeof(string)),
                    new DataColumn("Image", typeof(Icon))
                });
 
            _iconImagesTable.Columns["Identifier"].AutoIncrement = true;
            _iconImagesTable.Columns["Identifier"].AutoIncrementSeed = 1;
 
            var properties = typeof(Resources).GetProperties(
                BindingFlags.NonPublic |
                BindingFlags.Instance |
                BindingFlags.Static);
 
            var propertyInfos = (
                from T in properties
                where T.PropertyType == typeof(Icon)
                select T).ToList();
 
            if (propertyInfos.Count <= 0) return;
 
            foreach (var propertyInfo in propertyInfos)
            {
 
                _iconImagesTable.Rows.Add(null, propertyInfo.Name.Replace("_", " "),
                    Resources.ResourceManager.GetObject(propertyInfo.Name));
 
            }
   
        }
 
        private ResourceImages()
        {
        }
    }
 
}

In a form shown event return icons is simple as calling IconDataTable off the Instance property of the class ResourceImages.

var icons =  ResourceImages.Instance.IconDataTable();

To allow the user to select icons, place them into a ListBox.

iconImageListBox.DataSource = icons;
iconImageListBox.DisplayMember = "name";

Next for this demonstration show the key for the image and the image in a PictureBox.

iconPictureBox1.DataBindings.Add("image", icons, "Image");
IconIdentifierLabel.DataBindings.Add("text", icons, "Identifier");
 
The following screenshot shows a ListBox with icon names, a PictureBox with the currently selected icon along with the currently selected icon as the form icon.



To retrieve the project bitmap resources is the same as icons, from Instance property of ResourceImages call BitMapDataTable.

var bitmaps =  ResourceImages.Instance.BitMapDataTable();

Assign the bitmap images to a ListBox, setup data binding to a PictureBox to display the currently selected image from the ListBox along with data binding to a label to show the key.

bitMapListBox.DataSource = bitmaps;
bitMapListBox.DisplayMember = "name";
 
bitMapPictureBox1.DataBindings.Add("image", bitmaps, "Image");
Identifier.DataBindings.Add("text", bitmaps, "Identifier");

Screenshot:

Version 2 List containers

Although this article is an exploration for singletons there is room for improvement on the code which is responsible for retrieving images from resources. Although a DataTable is perfectly capable (as shown above) for these operations it's important to consider what is in memory at any given time especially on an application which may already to using a good deal of memory and system resources. A DataTable has methods and events which are simply not needed, a better choice would be to use a class were images would be stored in a list.

In the DataTable example there were two separate DataTable objects, one for bitmap, one for icon. In this case there will be a class for bitmap and one for icon which inherit from a base image class which has common properties. This could also be done with an Interface yet this would be overkill for this example.

Base image class
namespace ProjectResources.Classes
{
    public class BaseImage
    {
        public int Identifier { get; set; }
        public string Name { get; set; }
    }
}

Bitmap class
using System.Drawing;
 
namespace ProjectResources.Classes
{
    public class BitMapItem : BaseImage
    {
        public Image Image { get; set; }
    }
}

Icon class
using System.Drawing;
 
namespace ProjectResources.Classes
{
    public class IconItem : BaseImage
    {
        public Icon Image { get; set; }
    }
}

Consider breaking down classes for commonality, in the DataTable example there is a method to get a Icon from resources by string name which the class inherits from the following class.

using System.Drawing;
using System.Reflection;
 
namespace ProjectResources.Classes
{
    public class ImageUtility
    {
        /// <summary>
        /// Get Icon by string name, if name does not exists
        /// a runtime error is thrown
        /// </summary>
        /// <param name="imageName"></param>
        /// <returns></returns>
        public Icon GetImageByName(string imageName)
        {
            var asm = Assembly.GetExecutingAssembly();
            var resourceName = $"{asm.GetName().Name}.Properties.Resources";
            var rm = new System.Resources.ResourceManager(resourceName, asm);
            return (Icon)rm.GetObject(imageName);
        }
 
    }
}

Usage

public sealed class ResourceImages : ImageUtility
. . .
public sealed class ProjectClassResources : ImageUtility

Here is the class using list rather than DataTable for storing images in a singleton class.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Reflection;
using ProjectResources.Properties;
 
namespace ProjectResources.Classes
{
    public sealed class ProjectClassResources : ImageUtility
    {
        private static readonly Lazy<ProjectClassResources> Lazy =
            new Lazy<ProjectClassResources>(() => new ProjectClassResources());
 
        public static ProjectClassResources Instance => Lazy.Value;
 
        private List<IconItem> _iconImagesList;
        public List<IconItem> IconDataTable()
        {
            if (_iconImagesList == null)
            {
                GetIconImages();
            }
 
            return _iconImagesList;
 
        }
 
        private void GetIconImages()
        {
            _iconImagesList = new List<IconItem>();
 
            var properties = typeof(Resources).GetProperties(
                BindingFlags.NonPublic |
                BindingFlags.Instance |
                BindingFlags.Static);
 
            var propertyInfos = (
                from T in properties
                where T.PropertyType == typeof(Icon)
                select T).ToList();
 
            if (propertyInfos.Count <= 0) return;
 
            var index = 1;
            foreach (var propertyInfo in propertyInfos)
            {
                _iconImagesList.Add(new IconItem()
                {
                    Identifier = index,
                    Name = propertyInfo.Name.Replace("_", " "),
                    Image = (Icon)Resources.ResourceManager.GetObject(propertyInfo.Name)
                });
 
                index += 1;
            }
 
        }
 
        private List<BitMapItem> _bitMapImagesList;
        private void GetBitMapImages()
        {
            _bitMapImagesList = new List<BitMapItem>();
 
            var properties = typeof(Resources)
                .GetProperties(
                    BindingFlags.NonPublic |
                    BindingFlags.Instance |
                    BindingFlags.Static);
 
            var bitMaps = (
                from T in properties
                where T.PropertyType == typeof(Bitmap)
                select T).ToList();
 
            if (bitMaps.Count <= 0) return;
 
            var index = 1;
            foreach (var propertyInfo in bitMaps)
            {
                _bitMapImagesList.Add(new BitMapItem()
                {
                    Identifier = index,
                    Name = propertyInfo.Name.Replace("_", " "),
                    Image = (Image)Resources.ResourceManager.GetObject(propertyInfo.Name)
                });
 
                index += 1;
            }
 
        }
 
        public List<BitMapItem> BitMapList()
        {
            if (_bitMapImagesList == null)
            {
                GetBitMapImages();
            }
 
            return _bitMapImagesList;
        }
        public ProjectClassResources()
        {
             
        }
    }
}

To retrieve icons with the above class

var icons = ProjectClassResources.Instance.IconDataTable();

Using the DataTable method

var icons =  ResourceImages.Instance.IconDataTable();

For the full code sample to allow readers to try out both methods simply comment out list for DataTable or DataTable for list using comments.

private void Form1_Shown(object sender, EventArgs e)
{
    //var icons = ProjectClassResources.Instance.IconDataTable();
    var icons =  ResourceImages.Instance.IconDataTable();
 
    //var bitmaps = ProjectClassResources.Instance.BitMapList();
    var bitmaps =  ResourceImages.Instance.BitMapDataTable();
 
    iconImageListBox.DataSource = icons;
    iconImageListBox.DisplayMember = "name";
 
    iconPictureBox1.DataBindings.Add("image", icons, "Image");
    IconIdentifierLabel.DataBindings.Add("text", icons, "Identifier");
 
    bitMapListBox.DataSource = bitmaps;
    bitMapListBox.DisplayMember = "name";
 
    bitMapPictureBox1.DataBindings.Add("image", bitmaps, "Image");
    Identifier.DataBindings.Add("text", bitmaps, "Identifier");
 
}

Since in the code sample when the selected index changes on the ListBox the icons specifically must be cast differently between DataRow and class instance done with a ternary operator.

private void IconImageListBox_SelectedIndexChanged(object sender, EventArgs e)
{
    var currentImageName = iconImageListBox.SelectedItem is DataRowView view ?
        view.Row.Field<string>("Name") :
        ((IconItem) iconImageListBox.SelectedItem).Name;
 
    Icon = ResourceImages.Instance.GetImageByName(currentImageName);
}

Conclusion

Singletons work great for storing static resources in a project although if the intent is too store a lot of large images then a different approach should be looked at.

Storing/passing string data

This section will cover
  • Storing string data
  • Remembering special folder locations
  • Passing data between forms.

Storing string data

Storage of string data may include reference information ranging from simple strings to complex objects. Suppose there were objects as per below.
[
    {
        "id": 1,
        "ApplicationName": "Stronghold",
        "ApplicationVersion": "0.35",
        "ApplicationKey": "e3bb2399-9547-4bb9-86f1-578f6468ac1b"
    },
    {
        "id": 2,
        "ApplicationName": "Karen Entity",
        "ApplicationVersion": "0.80",
        "ApplicationKey": "fa6d4c20-ba8d-493c-9823-0d2df307ea3f"
    },
    {
        "id": 3,
        "ApplicationName": "Fix San",
        "ApplicationVersion": "3.8.9",
        "ApplicationKey": "13a3e6d3-6ddc-498f-a3b1-209cfd58e187"
    },
    {
        "id": 4,
        "ApplicationName": "Sub-Ex",
        "ApplicationVersion": "8.2",
        "ApplicationKey": "07dea10e-f43a-432f-bccd-ad210fa13cf7"
    },
    {
        "id": 5,
        "ApplicationName": "Duobam",
        "ApplicationVersion": "0.79",
        "ApplicationKey": "0fd2a1dc-5ebb-4ec3-b54a-d8025370a0ab"
    },
    {
        "id": 6,
        "ApplicationName": "Duobam",
        "ApplicationVersion": "2.3",
        "ApplicationKey": "27f2a9b0-aa04-4cd9-894b-61d744a21ad8"
    },
    {
        "id": 7,
        "ApplicationName": "Zoolab",
        "ApplicationVersion": "7.56",
        "ApplicationKey": "bb81a236-b199-4bbe-a939-0b1be1c3157c"
    },
    {
        "id": 8,
        "ApplicationName": "Alpha",
        "ApplicationVersion": "6.06",
        "ApplicationKey": "b6a68d03-6afc-4e66-87c3-1bda272900fe"
    },
    {
        "id": 9,
        "ApplicationName": "Stim",
        "ApplicationVersion": "4.0.5",
        "ApplicationKey": "b84b6531-c88d-45b6-871b-69c3b820b4df"
    },
    {
        "id": 10,
        "ApplicationName": "Zoolab",
        "ApplicationVersion": "4.3",
        "ApplicationKey": "7aae9913-2d40-4856-bc9d-a5c85977243d"
    },
    {
        "id": 11,
        "ApplicationName": "Bitwolf",
        "ApplicationVersion": "3.4.7",
        "ApplicationKey": "272ddba9-0be9-4df5-ac2b-f0da0169e878"
    },
    {
        "id": 12,
        "ApplicationName": "Duobam",
        "ApplicationVersion": "0.31",
        "ApplicationKey": "e76c0462-0a54-46d9-81a2-17706246cbac"
    },
    {
        "id": 13,
        "ApplicationName": "Voltsillam",
        "ApplicationVersion": "4.85",
        "ApplicationKey": "68706686-9222-420a-90bb-f0bcb5c583d7"
    },
    {
        "id": 14,
        "ApplicationName": "Bytecard",
        "ApplicationVersion": "3.37",
        "ApplicationKey": "308f9ff4-f4d3-4a46-bb19-f2057877dbaa"
    },
    {
        "id": 15,
        "ApplicationName": "Ronstring",
        "ApplicationVersion": "1.5",
        "ApplicationKey": "ee99628b-fe00-4edb-86df-77ee8352eed1"
    },
    {
        "id": 16,
        "ApplicationName": "Biodex",
        "ApplicationVersion": "6.9",
        "ApplicationKey": "8f86a454-73eb-4143-be75-05762387ec22"
    },
    {
        "id": 17,
        "ApplicationName": "Bamity",
        "ApplicationVersion": "0.22",
        "ApplicationKey": "e9df90bf-02a3-4116-bdcc-9d32711b4f90"
    },
    {
        "id": 18,
        "ApplicationName": "Andalax",
        "ApplicationVersion": "4.1",
        "ApplicationKey": "b2977b3d-3113-4d0f-bb6c-10f2faae6757"
    },
    {
        "id": 19,
        "ApplicationName": "Biodex",
        "ApplicationVersion": "0.8.4",
        "ApplicationKey": "7d3b1e2a-d82c-417b-8099-8376084ffa9c"
    },
    {
        "id": 20,
        "ApplicationName": "Treeflex",
        "ApplicationVersion": "0.61",
        "ApplicationKey": "0a8a6f82-47f1-4257-b2ca-5622e1f1ab53"
    }
]

Starting off with the same pattern uses for the prior singleton class

public sealed class CachedInformation
{
    private static readonly Lazy<CachedInformation> Lazy =
        new Lazy<CachedInformation>(() =>
            new CachedInformation());
 
    public static CachedInformation Instance => Lazy.Value;

Then in the class constructor read in the json.

protected CachedInformation()
{
    var fileOperations = new FileOperations();
 
    _applications = fileOperations.LoadApplicationData(
        Properties.Settings.Default.AppDataFile);
}

Add a property to access the data
private readonly List<Application> _applications;
public List<Application> Applications => _applications;

Access the data.
dataGridView1.DataSource = CachedInformation.Instance.Applications;

Very simple but overall it would be easier to consider not using a singleton and load the data as needed unless the data is needed at all times.

Storing a path

Storing a path that is not known until runtime such as the folder which contains a user.config file which is needed in various forms and or classes would be a good choice to place into a singleton e.g.

protected CachedInformation()
{
     /*
     * Get configuration folder name for where user settings are stored.
     * This does not mean the folder exists so before attempting to access
     * use Directory.Exists
     */
    _configurationFolder = OpenExeConfiguration(
        PerUserRoamingAndLocal).FilePath.Replace("\\user.config", "");
}

Note some may consider having a property to determine if the folder exists which if the folder exists at app startup and is deleted the code will fail so best to perform assertions outside of the singleton class.

Storing a complex object version 1

A requirement is to pass a customer instance between forms. In this case two properties will get the point across.

namespace Cached.Classes
{
    public class Customer
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}

In the main form there are two TextBox controls, prior to showing the child form which needs this customer the singleton will remember first and last name using auto-properties then upon the child form closing assign the first and last name from the child form back to the same first and last name TextBoxes on the calling form. Note there is no validation performed as that is outside the scope of this example.

private void ProcessButton_Click(object sender, EventArgs e)
{
    /*
     * Set properties as they have no values the first time
     */
    CachedInformation.Instance.Customer.FirstName = FirstNameTextBox.Text;
    CachedInformation.Instance.Customer.LastName = LastNameTextBox.Text;
 
    var customerForm = new CustomerForm();
 
    try
    {
        customerForm.ShowDialog();
 
        /*
         * Get property values
         */
        FirstNameTextBox.Text = CachedInformation.Instance.Customer.FirstName;
        LastNameTextBox.Text = CachedInformation.Instance.Customer.LastName;
 
    }
    finally
    {
        customerForm.Dispose();
    }
}

In the child form shown event the first and last name properties are retrieved from the singleton class and assigned to the TextBox controls, on clicking a button the properties FirstName and LastName are updated in the singleton class.

using System;
using System.Windows.Forms;
using Cached.Classes;
 
namespace Cached
{
    public partial class CustomerForm : Form
    {
        public CustomerForm()
        {
            InitializeComponent();
            Shown += CustomerForm_Shown;
        }
        /// <summary>
        /// Get current property values for our Customer
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void CustomerForm_Shown(object sender, EventArgs e)
        {
 
            FirstNameTextBox.Text = CachedInformation.Instance.Customer.FirstName;
            LastNameTextBox.Text = CachedInformation.Instance.Customer.LastName;
 
        }
        /// <summary>
        /// Set properties for our Customer
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void SetButton_Click(object sender, EventArgs e)
        {
 
            CachedInformation.Instance.Customer
                .FirstName = FirstNameTextBox.Text;
 
            CachedInformation.Instance.Customer
                .LastName = LastNameTextBox.Text;
 
            Close();
        }
    }
}

Storing a complex object version 2

A singleton pattern is the wrong solution, instead there are various ways to achieve passing the customer between forms. In the child form pass an instance of the customer in the child form constructor then set the properties to the child's TextBox controls. Clicking a button to set form level instance of a customer to the values held in the two TextBox controls. 

Child form
using System;
using System.Windows.Forms;
using PassingInformationBetweenForms.Classes;
 
namespace PassingInformationBetweenForms
{
    public partial class CustomerForm : Form
    {
        public CustomerForm()
        {
            InitializeComponent();
        }
 
        public Customer Customer { get; set; }
        public CustomerForm(Customer customer)
        {
            InitializeComponent();
 
            Customer = customer;
 
            FirstNameTextBox.Text = Customer.FirstName;
            LastNameTextBox.Text = Customer.LastName;
 
        }
        /// <summary>
        /// Set properties for our Customer
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void SetButton_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrWhiteSpace(FirstNameTextBox.Text) || string.IsNullOrWhiteSpace(LastNameTextBox.Text))
            {
                DialogResult = DialogResult.Cancel;
            }
            else
            {
                Customer.FirstName = FirstNameTextBox.Text;
                Customer.LastName = LastNameTextBox.Text;
 
                DialogResult = DialogResult.OK;
            }
        }
    }
}

The parent form shows the child form, determines by the child form's DialogResult to use or not use the values from the public class in the child form.

using System;
using System.Windows.Forms;
using PassingInformationBetweenForms.Classes;
 
namespace PassingInformationBetweenForms
{
    public partial class Form1 : Form
    {
        private Customer _customer = new Customer();
        public Form1()
        {
            InitializeComponent();
 
            _customer.FirstName = "Mary";
            _customer.LastName = "Adams";
 
            FirstNameTextBox.Text = _customer.FirstName;
            LastNameTextBox.Text = _customer.LastName;
        }
 
        private void OpenChildForm_Click(object sender, EventArgs e)
        {
            var customerForm = new CustomerForm(_customer);
 
            try
            {
                if (customerForm.ShowDialog() == DialogResult.OK)
                {
                    FirstNameTextBox.Text = customerForm.Customer.FirstName;
                    LastNameTextBox.Text = customerForm.Customer.LastName; ;
 
                    _customer.FirstName = customerForm.Customer.FirstName;
                    _customer.LastName = customerForm.Customer.LastName;
 
                }
            }
            finally
            {
                customerForm.Dispose();
            }
        }
    }
}

Conclusions

Before considering a singleton, look at other solutions which many times will not be in a developers comfort zone.

Fringe case

A Windows Service sends out email messages to report progress on scheduled jobs performed were the host incorrectly sees messages with the exact same subject line multiple times and does not send some of the messages. A solution is to use logic which will auto-increment a value which is appended to each email subject line. The appended text is not meaningful and will never be questioned by the receiver.

Singleton class which generates sequential values e.g. REF:0001 through REF:9000 then reset to REF:0001. Additionally the code runs in a multithreaded environment.

using System;
using System.Collections.Generic;
using System.Linq;
 
namespace Subject
{
    /// <summary>
    /// Thread safe singleton responsible for creating
    /// unique sequence for email subject.
    /// </summary>
    public sealed class ReferenceIncrementer
    {
        private static readonly Lazy<ReferenceIncrementer> Lazy =
            new Lazy<ReferenceIncrementer>(() =>
                new ReferenceIncrementer());
 
        public static ReferenceIncrementer Instance => Lazy.Value;
 
        private List<int> _baseList = new List<int>();
 
        /// <summary>
        /// Populate HashSet with random numbers.
        /// HastSet items are unique.
        /// </summary>
        private void CreateList()
        {
            _baseList = new List<int>();
 
            for (var index = 1; index < 9000; index++)
            {
                _baseList.Add(index);
            }
        }
        /// <summary>
        /// Return a left padded number prefix with REF: 0001
        /// .Any ask if there are any values when called.
        /// </summary>
        /// <returns></returns>
        public string GetReferenceValue()
        {
            if (!_baseList.Any())
            {
                CreateList();
            }
 
            var number = _baseList.FirstOrDefault();
            _baseList.Remove(number);
 
            return $" REF: {number:D4}";
 
        }
 
        /// <summary>
        /// Instantiate List
        /// </summary>
        private ReferenceIncrementer()
        {
            CreateList();
        }
        /// <summary>
        /// Used to reset at a given time e.g. right before midnight,
        /// perhaps by a scheduled job.
        /// </summary>
        public void Reset()
        {
            CreateList();
        }
    }
}

To get a value.
ReferenceIncrementer.Instance.GetReferenceValue()

Note: The above class has been running in a production Windows Server for over one year without any issues.

Since this singleton class property is in a Windows Service a unit test should be done such as shown below.
[TestMethod]
public void ValidateSubjectIncrementerTest()
{
    var emailSubject = "This is a test email ";
    var actualList = new List<string>();
    var expectedLastMessage = "This is a test email  REF: 0089";
 
    for (var index = 1; index < 90; index++)
    {
        actualList.Add(
            $"{emailSubject}{ReferenceIncrementer.Instance.GetReferenceValue()}");
 
    }
 
    Assert.AreEqual(expectedLastMessage, actualList.Last());
}


Summary

Singleton pattern gets a bad rap usually because a developer misuses the singleton pattern no different than perhaps using an adapter pattern which convert an interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces. Developer love seeing a new shiny object and are compelled to wedge the shiny object without thinking of "is it good or bad" into their code. Developers need to use the right tool for the task at hand. When considering using a singleton look at alternative paths before settling on a singleton, no different from using a Microsoft Access database instead of a SQL-Server database because of the familiarity of one over the other.
  • Singletons are misused and abused by less capable programmers and so everything becomes a singleton
  • Singletons are essentially static classes, relying on one or more static methods and properties. All things static present real, tangible problems when you try to do Unit Testing because they represent dead ends in your code that cannot be mocked or stubbed. This of course leads to is unit testing being done?

See also

A Thread Safe Singleton Cache Helper Class
C#: Singleton pattern with quick code snippet
Creational Design Pattern

Source code

GitHub repository