Featured desktop project

Build a full inventory management system in C# with Windows Forms and SQL Server

This hands-on project walks you through a complete desktop business application. You will create the database, design the form, connect to SQL Server, display data in a DataGridView, and code the add, update, delete, search, and clear functions that make the application useful in real work.

1 DatabaseProducts table in SQL Server
6 ActionsLoad, add, update, delete, search, export
Real UIForm inputs plus DataGridView
Portfolio ReadyA project you can show to others

A practical desktop application that teaches real development

An inventory system is one of the best projects for learning C# desktop development because it combines interface design, business logic, validation, database work, and clear user workflows. Instead of building a toy example, you build something that resembles a real business application used to manage stock and product information.

In this project, users can enter product details, save them to SQL Server, display them in a grid, edit selected rows, delete records, search by product name or category, and export results to a CSV file for reporting.

Project requirements

  • Visual Studio 2026 with Windows Forms App support
  • SQL Server Express, SQL Server Developer Edition, or LocalDB
  • Basic C# knowledge: variables, methods, events, and forms
  • NuGet package: Microsoft.Data.SqlClient if your project does not already include SQL connectivity
Tip: Keep this first version as a single-form application. Once it works well, you can split it into additional forms such as login, dashboard, reports, or supplier management.

Create the SQL Server database

Open SQL Server Management Studio or your preferred SQL tool and run the script below. It creates a simple database named InventoryDB and a Products table with the fields used by the Windows Forms app.

CREATE DATABASE InventoryDB;
GO

USE InventoryDB;
GO

CREATE TABLE Products
(
    ProductID INT IDENTITY(1,1) PRIMARY KEY,
    ProductName NVARCHAR(100) NOT NULL,
    Category NVARCHAR(50) NOT NULL,
    UnitPrice DECIMAL(10,2) NOT NULL,
    Quantity INT NOT NULL,
    ReorderLevel INT NOT NULL DEFAULT 5
);
GO

INSERT INTO Products (ProductName, Category, UnitPrice, Quantity, ReorderLevel)
VALUES
('Wireless Mouse', 'Accessories', 45.90, 24, 5),
('Mechanical Keyboard', 'Accessories', 189.00, 12, 4),
('27-inch Monitor', 'Displays', 899.00, 8, 3);
GO

This structure is simple enough for beginners, but strong enough to demonstrate real CRUD operations and stock calculations.

Design the Windows Forms interface

Create a new Windows Forms App in Visual Studio and name it InventoryManagementSystem. Add these controls to the main form.

Control Name Purpose
TextBox txtProductID Shows the selected product ID. Set it to ReadOnly = true.
TextBox txtProductName Stores the product name.
ComboBox cmbCategory Lets the user choose a category such as Accessories, Displays, Storage, or Office.
TextBox txtUnitPrice Stores the product price.
TextBox txtQuantity Stores the quantity in stock.
TextBox txtReorderLevel Stores the reorder threshold.
TextBox txtSearch Search box for product name or category.
Button btnAdd, btnUpdate, btnDelete, btnClear, btnSearch, btnExportCsv Triggers user actions.
DataGridView dgvProducts Displays all products and lets the user select a row for editing.
Label lblStatus Displays status messages such as “Product added successfully”.

Set the SelectionMode of the DataGridView to FullRowSelect and disable multi-select to make editing easier.

Main form code

Add the code below to your main form file. This version uses parameterized queries for safety, loads all products into the DataGridView, validates input, and supports the core inventory workflow.

using Microsoft.Data.SqlClient;
using System;
using System.Data;
using System.Globalization;
using System.IO;
using System.Text;
using System.Windows.Forms;

namespace InventoryManagementSystem
{
    public partial class Form1 : Form
    {
        private readonly string connectionString =
            @"Server=.\SQLEXPRESS;Database=InventoryDB;Trusted_Connection=True;TrustServerCertificate=True;";

        public Form1()
        {
            InitializeComponent();
            Load += Form1_Load;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            cmbCategory.Items.AddRange(new string[]
            {
                "Accessories",
                "Displays",
                "Storage",
                "Office",
                "Networking",
                "Components"
            });

            dgvProducts.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
            dgvProducts.MultiSelect = false;
            dgvProducts.ReadOnly = true;
            dgvProducts.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;

            LoadProducts();
            ClearFields();
        }

        private void LoadProducts(string keyword = "")
        {
            using SqlConnection conn = new SqlConnection(connectionString);
            using SqlCommand cmd = new SqlCommand();
            cmd.Connection = conn;

            if (string.IsNullOrWhiteSpace(keyword))
            {
                cmd.CommandText = @"SELECT ProductID, ProductName, Category, UnitPrice, Quantity, ReorderLevel,
                                           (UnitPrice * Quantity) AS StockValue
                                    FROM Products
                                    ORDER BY ProductName";
            }
            else
            {
                cmd.CommandText = @"SELECT ProductID, ProductName, Category, UnitPrice, Quantity, ReorderLevel,
                                           (UnitPrice * Quantity) AS StockValue
                                    FROM Products
                                    WHERE ProductName LIKE @keyword OR Category LIKE @keyword
                                    ORDER BY ProductName";
                cmd.Parameters.AddWithValue("@keyword", "%" + keyword + "%");
            }

            DataTable table = new DataTable();
            using SqlDataAdapter adapter = new SqlDataAdapter(cmd);
            adapter.Fill(table);
            dgvProducts.DataSource = table;
            HighlightLowStockRows();
            lblStatus.Text = table.Rows.Count + " product(s) loaded.";
        }

        private void HighlightLowStockRows()
        {
            foreach (DataGridViewRow row in dgvProducts.Rows)
            {
                if (row.Cells["Quantity"].Value != null && row.Cells["ReorderLevel"].Value != null)
                {
                    int quantity = Convert.ToInt32(row.Cells["Quantity"].Value);
                    int reorderLevel = Convert.ToInt32(row.Cells["ReorderLevel"].Value);

                    if (quantity <= reorderLevel)
                    {
                        row.DefaultCellStyle.BackColor = System.Drawing.Color.MistyRose;
                    }
                    else
                    {
                        row.DefaultCellStyle.BackColor = System.Drawing.Color.White;
                    }
                }
            }
        }

        private bool ValidateInputs(out decimal unitPrice, out int quantity, out int reorderLevel)
        {
            unitPrice = 0;
            quantity = 0;
            reorderLevel = 0;

            if (string.IsNullOrWhiteSpace(txtProductName.Text))
            {
                MessageBox.Show("Please enter a product name.");
                txtProductName.Focus();
                return false;
            }

            if (cmbCategory.SelectedIndex == -1)
            {
                MessageBox.Show("Please select a category.");
                cmbCategory.Focus();
                return false;
            }

            if (!decimal.TryParse(txtUnitPrice.Text, NumberStyles.Number, CultureInfo.InvariantCulture, out unitPrice))
            {
                MessageBox.Show("Please enter a valid unit price.");
                txtUnitPrice.Focus();
                return false;
            }

            if (!int.TryParse(txtQuantity.Text, out quantity))
            {
                MessageBox.Show("Please enter a valid quantity.");
                txtQuantity.Focus();
                return false;
            }

            if (!int.TryParse(txtReorderLevel.Text, out reorderLevel))
            {
                MessageBox.Show("Please enter a valid reorder level.");
                txtReorderLevel.Focus();
                return false;
            }

            if (unitPrice < 0 || quantity < 0 || reorderLevel < 0)
            {
                MessageBox.Show("Numeric values cannot be negative.");
                return false;
            }

            return true;
        }

        private void ClearFields()
        {
            txtProductID.Clear();
            txtProductName.Clear();
            cmbCategory.SelectedIndex = -1;
            txtUnitPrice.Clear();
            txtQuantity.Clear();
            txtReorderLevel.Text = "5";
            txtSearch.Clear();
            txtProductName.Focus();
        }

        private void btnAdd_Click(object sender, EventArgs e)
        {
            if (!ValidateInputs(out decimal unitPrice, out int quantity, out int reorderLevel))
                return;

            using SqlConnection conn = new SqlConnection(connectionString);
            using SqlCommand cmd = new SqlCommand(
                @"INSERT INTO Products (ProductName, Category, UnitPrice, Quantity, ReorderLevel)
                  VALUES (@ProductName, @Category, @UnitPrice, @Quantity, @ReorderLevel)", conn);

            cmd.Parameters.AddWithValue("@ProductName", txtProductName.Text.Trim());
            cmd.Parameters.AddWithValue("@Category", cmbCategory.Text);
            cmd.Parameters.AddWithValue("@UnitPrice", unitPrice);
            cmd.Parameters.AddWithValue("@Quantity", quantity);
            cmd.Parameters.AddWithValue("@ReorderLevel", reorderLevel);

            conn.Open();
            cmd.ExecuteNonQuery();

            LoadProducts();
            ClearFields();
            lblStatus.Text = "Product added successfully.";
        }

        private void btnUpdate_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrWhiteSpace(txtProductID.Text))
            {
                MessageBox.Show("Please select a product to update.");
                return;
            }

            if (!ValidateInputs(out decimal unitPrice, out int quantity, out int reorderLevel))
                return;

            using SqlConnection conn = new SqlConnection(connectionString);
            using SqlCommand cmd = new SqlCommand(
                @"UPDATE Products
                  SET ProductName = @ProductName,
                      Category = @Category,
                      UnitPrice = @UnitPrice,
                      Quantity = @Quantity,
                      ReorderLevel = @ReorderLevel
                  WHERE ProductID = @ProductID", conn);

            cmd.Parameters.AddWithValue("@ProductID", Convert.ToInt32(txtProductID.Text));
            cmd.Parameters.AddWithValue("@ProductName", txtProductName.Text.Trim());
            cmd.Parameters.AddWithValue("@Category", cmbCategory.Text);
            cmd.Parameters.AddWithValue("@UnitPrice", unitPrice);
            cmd.Parameters.AddWithValue("@Quantity", quantity);
            cmd.Parameters.AddWithValue("@ReorderLevel", reorderLevel);

            conn.Open();
            cmd.ExecuteNonQuery();

            LoadProducts();
            ClearFields();
            lblStatus.Text = "Product updated successfully.";
        }

        private void btnDelete_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrWhiteSpace(txtProductID.Text))
            {
                MessageBox.Show("Please select a product to delete.");
                return;
            }

            DialogResult confirm = MessageBox.Show(
                "Delete this product?",
                "Confirm Delete",
                MessageBoxButtons.YesNo,
                MessageBoxIcon.Warning);

            if (confirm != DialogResult.Yes)
                return;

            using SqlConnection conn = new SqlConnection(connectionString);
            using SqlCommand cmd = new SqlCommand(
                "DELETE FROM Products WHERE ProductID = @ProductID", conn);

            cmd.Parameters.AddWithValue("@ProductID", Convert.ToInt32(txtProductID.Text));

            conn.Open();
            cmd.ExecuteNonQuery();

            LoadProducts();
            ClearFields();
            lblStatus.Text = "Product deleted successfully.";
        }

        private void btnSearch_Click(object sender, EventArgs e)
        {
            LoadProducts(txtSearch.Text.Trim());
        }

        private void btnClear_Click(object sender, EventArgs e)
        {
            ClearFields();
            LoadProducts();
            lblStatus.Text = "Form cleared.";
        }

        private void dgvProducts_CellClick(object sender, DataGridViewCellEventArgs e)
        {
            if (e.RowIndex < 0) return;

            DataGridViewRow row = dgvProducts.Rows[e.RowIndex];
            txtProductID.Text = row.Cells["ProductID"].Value?.ToString();
            txtProductName.Text = row.Cells["ProductName"].Value?.ToString();
            cmbCategory.Text = row.Cells["Category"].Value?.ToString();
            txtUnitPrice.Text = row.Cells["UnitPrice"].Value?.ToString();
            txtQuantity.Text = row.Cells["Quantity"].Value?.ToString();
            txtReorderLevel.Text = row.Cells["ReorderLevel"].Value?.ToString();
        }

        private void btnExportCsv_Click(object sender, EventArgs e)
        {
            if (dgvProducts.Rows.Count == 0)
            {
                MessageBox.Show("There is no data to export.");
                return;
            }

            using SaveFileDialog dialog = new SaveFileDialog();
            dialog.Filter = "CSV files (*.csv)|*.csv";
            dialog.FileName = "inventory-report.csv";

            if (dialog.ShowDialog() != DialogResult.OK)
                return;

            StringBuilder sb = new StringBuilder();

            for (int i = 0; i < dgvProducts.Columns.Count; i++)
            {
                sb.Append(dgvProducts.Columns[i].HeaderText);
                if (i < dgvProducts.Columns.Count - 1)
                    sb.Append(",");
            }
            sb.AppendLine();

            foreach (DataGridViewRow row in dgvProducts.Rows)
            {
                if (row.IsNewRow) continue;

                for (int i = 0; i < dgvProducts.Columns.Count; i++)
                {
                    sb.Append(row.Cells[i].Value?.ToString());
                    if (i < dgvProducts.Columns.Count - 1)
                        sb.Append(",");
                }
                sb.AppendLine();
            }

            File.WriteAllText(dialog.FileName, sb.ToString());
            lblStatus.Text = "CSV exported successfully.";
            MessageBox.Show("Export completed.");
        }
    }
}

This example gives you a complete working flow. After placing the controls on the form, make sure each button click event and the DataGridView cell click event are connected to the matching methods.

How the application works

Add products

Enter product details and click Add. The record is inserted into SQL Server and the grid refreshes automatically.

Edit records

Click a row in the grid to load it into the form. Modify the values and click Update.

Find and export

Use the search box to filter by name or category, then export the current grid contents to CSV for reporting.

Useful improvements for the next version

  • Add a login form for administrators and staff roles.
  • Create a dashboard showing total products, low-stock items, and total stock value.
  • Add supplier and purchase tables so the app handles inventory flow more realistically.
  • Generate printable reports with category summaries or monthly stock movement.
  • Use a separate data access class so the project structure is cleaner and easier to maintain.
Best practice: Once the project works, refactor repeated database logic into helper classes or a repository class. That makes the code easier to test, reuse, and expand.

Test checklist

  • Add a product and confirm it appears in the DataGridView.
  • Select a row, update the quantity, and confirm the change is saved.
  • Delete a selected row and confirm it disappears from the grid.
  • Search by category and confirm the results are filtered correctly.
  • Enter invalid values such as letters in the price field and confirm validation prevents saving.
  • Reduce quantity below the reorder level and confirm the row is highlighted as low stock.
  • Export to CSV and confirm the file opens correctly in Excel or another spreadsheet app.

Where to go next

This project is a strong bridge between beginner Windows Forms practice and larger business software. After completing it, you can expand into database reporting, dashboards, authentication, or web APIs for connected desktop applications.

Read why desktop C# development still matters →

Build sequence

A simple order for completing the project

Create the database first, design the form second, code the CRUD operations third, and only then add search, CSV export, and visual enhancements. That order keeps the project manageable and helps you troubleshoot problems step by step.

Related desktop pages

Continue with the rest of the desktop track