[go: up one dir, main page]

DEV Community

Cover image for WinForms dynamic buttons
Karen Payne
Karen Payne

Posted on

WinForms dynamic buttons

Introduction

Learn how to create a series of buttons at runtime which is useful for displaying information were the data changes, for instance, product categories, images or asking questions. There are three projects, two projects read data from a SQL Server database, and one reads json data. In each of the projects there is a common pattern used which means that code will be easy to follow from project to project.

An important thing here in all code samples is to keep code in each form to a minimal which means, as done when writing the provided code a developer can easily copy-and-paste code to another project and make changes rather than copy code from a form where there may be ties to the form.

Source code

Working with databases

When working with SQL Server data, SQL statements are stored in read only strings for the reader to easily see the statements. Some might consider using stored procedures for security.

Connection strings are stored in appsettings.json

Custom Button

While most developers needing to store project specific information for a button use the tag property of, in this case a button, here a custom is used.

Button properties

  • Identifier for storing a primary key
  • Stash for storing anything (but not used, here to give possiblities)
  • Project specific property e.g. Image, a class instance
public class DataButton : Button
{
    [Category("Behavior"), Description("Identifier")]
    public int Identifier { get; set; }
    [Category("Behavior"), Description("Stash")]
    public string Stash { get; set; }

    public Image Picture { get; set; }
}

public class DataButton : Button
{
    [Category("Behavior"), Description("Identifier")]
    public int Identifier { get; set; }
    [Category("Behavior"), Description("Stash")]
    public string Stash { get; set; }

    public new Container Container { get; set; }
}

public class DataButton : Button
{
    [Category("Behavior"), Description("Identifier")]
    public int Identifier { get; set; }
    [Category("Behavior"), Description("Stash")]
    public string Stash { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Common classes

Consistency is important when writing similar task. For each project there are two classes, ButtonSetup used to feed information to ButtonOperations which is responsible for button creation.

➰ In each project, positioning an button sizes vary.

ButtonSetup

public class ButtonSetup
{
    public Control Control { get; set; }
    public int Top { get; set; }
    public int BaseHeightPadding { get; set; }
    public int Left { get; set; }
    public int Width { get; set; }
    public EventHandler ButtonClick { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Which is used to feed information to ButtonOperations constructor. Note, Log.Information which uses SeriLog NuGet packages to write details to a file which is useful if there is an issue with displaying buttons, remove this for production code.

public static void Initialize(ButtonSetup sender)
{

    ParentControl = sender.Control;
    Top = sender.Top;
    HeightPadding = sender.BaseHeightPadding;
    Left = sender.Left;
    Width = sender.Width;
    EventHandler = sender.ButtonClick;
    ButtonsList = new List<DataButton>();

    var methodName = $"{nameof(ButtonOperations)}.{nameof(Initialize)}";
    Log.Information("{Caller} Top: {Top} Left: {Left}", methodName, sender.Top, sender.Left);

}
Enter fullscreen mode Exit fullscreen mode

Displaying images

In this example a database table has images a developer wants to display and the count of images can change.

Form with buttons to display images

Data Operations

The following statements are stored in a class to read data.

public static string SelectImageByIdentifier => 
    """
    SELECT 
        id, 
        Photo,
        Description
    FROM 
        dbo.Pictures1 
    WHERE 
        dbo.Pictures1.id = @id;
    """;

public static string ReadAllImages => 
    """
    SELECT 
        Id,
        Photo,
        [Description] 
    FROM 
        dbo.Pictures1
    """;
Enter fullscreen mode Exit fullscreen mode

Model for storing data with the Image property converting a byte array to an image with an extension method.

public class PhotoContainer
{
    public int Id { get; set; }
    public Image Image => Photo.BytesToImage();
    public Image Picture { get; set; }
    public byte[] Photo { get; set; }
    public string Description { get; set; }
    public override string ToString() => Description;
}
Enter fullscreen mode Exit fullscreen mode

Class to read data from SQL-Server using Dapper NuGet package.

internal class PhotoOperations
{

    /// <summary>
    /// Read image from database table by primary key
    /// </summary>
    /// <param name="identifier">Primary key</param>
    public static (bool success, PhotoContainer) ReadImage(int identifier)
    {
        using var cn = new SqlConnection(ConnectionString());
        var container =  cn.QueryFirstOrDefault<PhotoContainer>(
            SqlStatements.SelectImageByIdentifier, new {id = identifier});
        if (container is not null)
        {
            container.Picture = container.Photo.BytesToImage();

            return (true, container);
        }

        return (false, null);
    }

    /// <summary>
    /// Read all records for images
    /// </summary>
    public static List<PhotoContainer> Read()
    {
        using var cn = new SqlConnection(ConnectionString());
        return  cn.Query<PhotoContainer>(SqlStatements.ReadAllImages).AsList();
    }
}
Enter fullscreen mode Exit fullscreen mode

Button creation class

  • Parent property is the control to add buttons too.
  • Identifier, primary key used to reference SQL-Server data.
  • Name, always name controls as a reference to a button may be needed.
  • EventHandler which provide access to a button Click event.
  • Log.Information is for development, remove for production.
public static class ButtonOperations
{
    public static List<DataButton> ButtonsList { get; set; }
    public static int Top { get; set; }
    public static int Left { get; set; }
    public static int Width { get; set; }
    public static int HeightPadding { get; set; }
    public static string BaseName { get; set; } = "Button";
    public static EventHandler EventHandler { get; set; }
    public static Control ParentControl { get; set; }
    private static int _index = 1;

    public static void Initialize(ButtonSetup sender)
    {
        ParentControl = sender.Control;
        Top = sender.Top;
        HeightPadding = sender.BaseHeightPadding;
        Left = sender.Left;
        Width = sender.Width;
        EventHandler = sender.ButtonClick;
        ButtonsList = [];

        var methodName = $"{nameof(ButtonOperations)}.{nameof(Initialize)}";

        // allows developer to see what was created for debug purposes
        Log.Information("{Caller} Top: {Top} Left: {Left}", methodName, sender.Top, sender.Left);

    }
    /// <summary>
    /// Create new <see cref="DataButton"/> and add to <see cref="ButtonsList"/>"/> and set Click event
    /// </summary>
    /// <param name="description">Description of image</param>
    /// <param name="identifier">Primary key of row for image</param>
    private static void CreateButton(string description, int identifier)
    {

        var ( _, photoContainer) = PhotoOperations.ReadImage(identifier);
        var button = new DataButton()
        {
            Name = $"{BaseName}{_index}",
            Text = description,
            Width = Width,
            Height = 29,
            Location = new Point(Left, Top),
            Parent = ParentControl,
            Identifier = identifier,
            Visible = true, 
            Picture = photoContainer.Picture
        };

        button.Click += EventHandler;

        var methodName = $"{nameof(ButtonOperations)}.{nameof(CreateButton)}";

        // allows developer to see what was created for debug purposes
        Log.Information("{Caller} Name: {Name} CategoryId: {CategoryId} Location {Left},{Right}", 
            methodName, button.Name, identifier, Left, Top);

        ButtonsList.Add(button);

        ParentControl.Controls.Add(button);
        Top += HeightPadding;
        _index += 1;

    }

    public static void BuildButtons()
    {
        foreach (var container in PhotoOperations.Read())
        {
            CreateButton(container.Description, container.Id);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Form code

A FlowLayoutPanel will house buttons with a PictureBox below the FlowLayoutPanel (which is docked to form top).

Buttons are created followed by finding a specific button (DataButton) with specific text. This is followed by setting the Button's image to the PictureBox and then making the button the active control.

ButtonClick event is triggered when clicking any button (DataButton) and assigning it's image to the PictureBox.

Read from two table

This code sample reads from a modifies version of Microsoft's NorthWind database. Each category is displayed in buttons (DataButton), on click display products for the select category in a ListBox. Under the ListBox shows the product name and primary key. Double Click a product to get the primary key and product name. This allows a developer to write another statement to find out for instances orders that the product is in.

Shows the form

Models

public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
    public override string ToString() => Name;
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public override string ToString() => Name;
}
Enter fullscreen mode Exit fullscreen mode

Custom button

Note when compared to the last example has the same properties minus the Image property.

public class DataButton : Button
{
    [Category("Behavior"), Description("Identifier")]
    public int Identifier { get; set; }
    [Category("Behavior"), Description("Stash")]
    public string Stash { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Data Operations

Here Dapper is used again while rather than using another class for SQL Statements the statements are in each method which some developer might like instead of separating statements.

The pattern used here is similar to the last example minis images.

  • ReadCategories method reads all categories from the database.
  • ReadProducts method reads all products for a specific category.
public class DataOperations
{
    /// <summary>
    /// Read all categories
    /// </summary>
    /// <returns>list of categories</returns>
    public static List<Category> ReadCategories()
    {
        var list = new List<Category>();

        try
        {
            using SqlConnection cn = new() { ConnectionString = ConnectionString() };
            var selectStatement = 
                """
                SELECT CategoryID as Id, CategoryName as Name
                FROM dbo.Categories
                """;
            list = cn.Query<Category>(selectStatement).ToList();
        }
        catch (Exception exception)
        {
            Log.Error(exception.Message);
        }

        return list;

    }
    /// <summary>
    /// Read products by category identifier
    /// </summary>
    /// <param name="identifier">category identifier</param>
    /// <returns>list of products for category</returns>
    public static List<Product> ReadProducts(int identifier)
    {
        using SqlConnection cn = new() { ConnectionString = ConnectionString() };

        var selectStatement = 
            """
            SELECT ProductID as Id, ProductName as Name
            FROM dbo.Products WHERE CategoryID = @Id
            ORDER BY ProductName
            """;

        return cn.Query<Product>(selectStatement, new { Id = identifier }).ToList();

    }
}
Enter fullscreen mode Exit fullscreen mode

Button creation

Follows the same code as with the image sample.

  • Parent property is the control to add buttons too.
  • Identifier, primary key used to reference SQL-Server data.
  • Name, always name controls as a reference to a button may be needed.
  • EventHandler which provide access to a button Click event.
  • Log.Information is for development, remove for production.
public static class ButtonOperations
{
    public static List<DataButton> ButtonsList { get; set; }
    public static int Top { get; set; }
    public static int Left { get; set; }
    public static int Width { get; set; }
    public static int HeightPadding { get; set; }
    public static string BaseName { get; set; } = "Button";
    public static EventHandler EventHandler { get; set; }
    public static Control ParentControl { get; set; }
    private static int _index = 1;


    public static void Initialize(ButtonSetup sender)
    {

        ParentControl = sender.Control;
        Top = sender.Top;
        HeightPadding = sender.BaseHeightPadding;
        Left = sender.Left;
        Width = sender.Width;
        EventHandler = sender.ButtonClick;
        ButtonsList = new List<DataButton>();

        var methodName = $"{nameof(ButtonOperations)}.{nameof(Initialize)}";
        Log.Information("{Caller} Top: {Top} Left: {Left}", methodName, sender.Top, sender.Left);

    }

    private static void CreateCategoryButton(string text, int categoryIdentifier)
    {

        var button = new DataButton()
        {
            Name = $"{BaseName}{_index}",
            Text = text,
            Width = Width,
            Height = 29,
            Location = new Point(Left, Top),
            Parent = ParentControl,
            Identifier = categoryIdentifier,
            Visible = true,
            ImageAlign = ContentAlignment.MiddleLeft,
            TextAlign = ContentAlignment.MiddleRight
        };

        button.Click += EventHandler;

        var methodName = $"{nameof(ButtonOperations)}.{nameof(CreateCategoryButton)}";
        Log.Information("{Caller} Name: {Name} CategoryId: {CategoryId} Location {Left},{Right}", 
            methodName, button.Name, categoryIdentifier, Left, Top);

        ButtonsList.Add(button);

        ParentControl.Controls.Add(button);
        Top += HeightPadding;
        _index += 1;

    }

    public static void BuildButtons()
    {
        foreach (var category in DataOperations.ReadCategories())
        {
            CreateCategoryButton(category.Name, category.Id);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Form code

Here buttons are placed directly on the left side of the form unlike the last example which placed button in a FlowLayoutPanel. There is not much different from the last examle.

In the form constructor.

  • Setup for creating buttons.
  • Create the buttons and display the category buttons.

In the Click event for the buttons, the active button is assigned an image to show the active category from project resources.

➰ Note how clean and easy the code is to read!!!

public partial class Form1 : Form
{
    private BindingList<Product> productsBindingList;
    private readonly BindingSource productBindingSource = new ();
    public Form1()
    {
        InitializeComponent();

        ButtonOperations.BaseName = "CategoryButton";


        var buttonSetup = new ButtonSetup
        {
            Control = this,
            Top = 20,
            BaseHeightPadding = 42,
            Left = 10,
            Width = 150,
            ButtonClick = CategoryButtonClick
        };

        ButtonOperations.Initialize(buttonSetup);
        ProductsListBox.DoubleClick += ProductsListBoxOnDoubleClick;
        ButtonOperations.BuildButtons();
    }

    private void ProductsListBox_SelectedIndexChanged(object sender, EventArgs e)
    {
        DisplayCurrentProduct();
    }

    private void DisplayCurrentProduct()
    {
        if (productBindingSource.Current is null)
        {
            return;
        }

        var product = productsBindingList[productBindingSource.Position];
        CurrentProductTextBox.Text = $"{product.Id}, {product.Name}";
    }

    private void ProductsListBoxOnDoubleClick(object sender, EventArgs e)
    {
        if (productBindingSource.Current is null)
        {
            return;
        }

        var product = productsBindingList[productBindingSource.Position];

        MessageBox.Show($"{product.Id}, {product.Name}");
    }

    private void CategoryButtonClick(object sender, EventArgs e)
    {

        ButtonOperations.ButtonsList.ForEach(b => b.Image = null);

        var button = (DataButton)sender;

        button.Image = Resources.rightArrow24;

        ProductsListBox.SelectedIndexChanged -= ProductsListBox_SelectedIndexChanged;
        productsBindingList = new BindingList<Product>(DataOperations.ReadProducts(button.Identifier));
        productBindingSource.DataSource = productsBindingList;
        ProductsListBox.DataSource = productBindingSource;
        ProductsListBox.SelectedIndexChanged += ProductsListBox_SelectedIndexChanged;

        DisplayCurrentProduct();

    }
}
Enter fullscreen mode Exit fullscreen mode

Questions and answers

Although this code sample follows the same code patterns as the above samples there are more moving parts.

Data is read from a json file so that the reader can switch it up from using a database. There are over 500 questions and answers. For this example, 15 random questions are displayed.

Form screenshot

Model

  • The properties, A,B,C,D are choices
  • QuestionIdentifier property is the button text
  • Answer is one of the A,B,C,D properties
{
  "Id": 1,
  "Question": "A flashing red traffic light signifies that a driver should do what?",
  "A": "stop",
  "B": "speed up",
  "C": "proceed with caution",
  "D": "honk the horn",
  "Answer": "A"
}
Enter fullscreen mode Exit fullscreen mode
public class Container
{
    public int Id { get; set; }
    public int QuestionIdentifier { get; set; }
    public string Question { get; set; }
    public string A { get; set; }
    public string B { get; set; }
    public string C { get; set; }
    public string D { get; set; }
    public string Answer { get; set; }
    public override string ToString() => Question;
}
Enter fullscreen mode Exit fullscreen mode

Button operations

Again, use the same logic as the former examples. Note the use of Random.Shared.GetItems which provides random questions from the json file so each time the project runs different questions are shown.

public static class ButtonOperations
{
    public static List<DataButton> ButtonsList { get; set; }
    public static List<Container> Containers { get; set; }
    public static int Top { get; set; }
    public static int Left { get; set; }
    public static int Width { get; set; }
    public static int HeightPadding { get; set; }
    public static string BaseName { get; set; } = "Button";
    public static EventHandler EventHandler { get; set; }
    public static Control ParentControl { get; set; }
    private static int _index = 1;
    private static int _questionCount;


    public static void Initialize(ButtonSetup sender)
    {

        ParentControl = sender.Control;
        Top = sender.Top;
        HeightPadding = sender.BaseHeightPadding;
        Left = sender.Left;
        Width = sender.Width;
        EventHandler = sender.ButtonClick;
        ButtonsList = [];
        _questionCount = sender.QuestionCount;

        var methodName = $"{nameof(ButtonOperations)}.{nameof(Initialize)}";

        // allows developer to see what was created for debug purposes
        Log.Information("{Caller} Top: {Top} Left: {Left}", methodName, sender.Top, sender.Left);

    }

    private static void CreateButton(Container container)
    {
        var button = new DataButton()
        {
            Name = $"{BaseName}{_index}",
            Text = container.QuestionIdentifier.ToString(),
            Width = Width,
            Height = 29,
            Location = new Point(Left, Top),
            Parent = ParentControl,
            Identifier = container.Id,
            Container = container,
            Visible = true
        };

        button.Click += EventHandler;

        var methodName = $"{nameof(ButtonOperations)}.{nameof(CreateButton)}";

        // allows developer to see what was created for debug purposes
        Log.Information("{Caller} Name: {Name} Id: {CategoryId} Location {Left},{Right}", 
            methodName, button.Name, container.Id, Left, Top);

        ButtonsList.Add(button);

        ParentControl.Controls.Add(button);
        Top += HeightPadding;
        _index += 1;

    }

    public static void BuildButtons()
    {
        Containers = Random.Shared
            .GetItems<Container>(CollectionsMarshal.AsSpan(JsonOperations.GetQuestions()), _questionCount)
            .ToList();

        for (int index = 0; index < Containers.Count; index++)
        {
            Containers[index].QuestionIdentifier = index + 1;
        }

        foreach (var container in Containers)
        {
            CreateButton(container);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Read Json file class

public class JsonOperations
{
    public static string FileName => "questions.json";

    public static List<Container> GetQuestions() 
        => JsonSerializer.Deserialize<List<Container>>(File.ReadAllText(FileName));
}
Enter fullscreen mode Exit fullscreen mode

DataButton

In this case the question/answers are included in the property Container.

public class DataButton : Button
{
    [Category("Behavior"), Description("Identifier")]
    public int Identifier { get; set; }
    [Category("Behavior"), Description("Stash")]
    public string Stash { get; set; }

    public new Container Container { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Form Code

  • As with the former code samples, buttons are created using the same logic. Controls are added to a FlowLayoutPanel.
  • Four RadioButtons are data bound to A,B,C,D choices for the current question.
  • A label is data bound to the current question.
  • Container _currentContainer is used to keep track of the current question.

ButtonClick event

This event sets up the correct answer by setting the RadioButton Tag property to not null.

AnswerButton_Click event

Checks to see if the correct RadioButton was clicked.

public partial class Form1 : Form
{
    private BindingList<Container> _containers = new();
    private BindingSource _bindingSource = new();
    private Container _currentContainer;

    public Form1()
    {
        InitializeComponent();

        ButtonOperations.BaseName = "CategoryButton";

        var buttonSetup = new ButtonSetup
        {
            Control = this,
            Top = 20,
            BaseHeightPadding = 42,
            Left = 10,
            Width = 150,
            ButtonClick = ButtonClick,
            QuestionCount = 15
        };

        ButtonOperations.Initialize(buttonSetup);
        ButtonOperations.BuildButtons();
        var buttons = ButtonOperations.ButtonsList;

        foreach (var button in ButtonOperations.ButtonsList)
        {
            flowLayoutPanel1.Controls.Add(button);
        }

        _containers = new BindingList<Container>(ButtonOperations.Containers);
        _bindingSource.DataSource = _containers;

        QuestionLabel.DataBindings.Add("Text", _bindingSource, "Question");
        radioButtonA.DataBindings.Add("Text", _bindingSource, "A");
        radioButtonB.DataBindings.Add("Text", _bindingSource, "B");
        radioButtonC.DataBindings.Add("Text", _bindingSource, "C");
        radioButtonD.DataBindings.Add("Text", _bindingSource, "D");

        _currentContainer = _containers[0];

        var answer = _currentContainer.Answer;
        var rb = QuestionGroupBox.Controls.OfType<RadioButton>()
            .FirstOrDefault(x => x.Name.EndsWith(answer));

        rb!.Tag = "Correct";
    }

    private void ButtonClick(object? sender, EventArgs e)
    {
        // clear all tags used in the button click event to determine correct answer
        foreach (var radioButton in QuestionGroupBox.Controls.OfType<RadioButton>())
        {
            radioButton.Tag = null;
        }

        var button = (DataButton)sender!;
        _currentContainer = button.Container;

        // set the correct answer for the radio button
        var rb = QuestionGroupBox.Controls.OfType<RadioButton>()
            .FirstOrDefault(x => x.Name.EndsWith(_currentContainer.Answer));
        rb!.Tag = "Correct";

        // set the position of the binding source to the current container/question
        _bindingSource.Position = _containers.IndexOf(_currentContainer);

        foreach (var radioButton in QuestionGroupBox.RadioButtonList())
        {
            radioButton.Checked = false;
        }

    }

    private void AnswerButton_Click(object sender, EventArgs e)
    {
        var answer = QuestionGroupBox.RadioButtonChecked();
        if (answer is null) return;
        if (answer.Tag is not null)
        {
            MessageBox.Show("Correct");  
        }
        else
        {
            MessageBox.Show("Incorrect");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Summary

Several code samples have been provided to show how to create dynamic buttons at runtime for Window Forms uses similar/consistent code patterns which should educate a developer to then follow what has been provided for their task.

If something does not make sense, set breakpoints and traverse the code and inspect object and variables for a better understanding of the code.

Top comments (2)

Collapse
 
jangelodev profile image
João Angelo

Hi Karen Payne,
Top, very nice and helpful !
Thanks for sharing. ✅

Collapse
 
karenpayneoregon profile image
Karen Payne

Your welcome.