Exporting objects in various formats while reporting progress Description
A WinForms application has a function to export objects of the following type, in various formats:
class Item
{
    public int id { get; set; }
    public string description { get; set; }
}

At the click of a button in a window, a SaveFileDialog is shown, and currently it provides the option to save the data in .txt, .csv or .xlsx format. Since there are sometimes hundreds or thousands of objects, and the UI should not freeze up, a Task is used to run this operation. This implementation works, but could be improved.
Code
public partial class ExportWindow : Form
{
    // objects to be exported
    List&lt;Item&gt; items;

    // event handler for the &quot;Export&quot; button click
    private async void exportButton_click(object sender, System.EventArgs e)
    {
        SaveFileDialog exportDialog = new SaveFileDialog();
        exportDialog.Filter = &quot;Text File (*.txt)|*.txt|Comma-separated values file (*.csv)|*.csv|Excel spreadsheet (*.xlsx)|*.xlsx&quot;;
        exportDialog.CheckPathExists = true;
        DialogResult result = exportDialog.ShowDialog();
        if (result == DialogResult.OK)
        {
            var ext = System.IO.Path.GetExtension(saveExportFileDlg.FileName);

            try
            { 
                // update status bar
                // (it is a custom control)
                statusBar.text(&quot;Exporting&quot;);

                // now export it
                await Task.Run(() =&gt;
                {
                    switch (ext.ToLower())
                    {
                        case &quot;.txt&quot;:
                            saveAsTxt(exportDialog.FileName);
                            break;

                        case &quot;.csv&quot;:
                            saveAsCsv(exportDialog.FileName);
                            break;
                    
                        case &quot;.xlsx&quot;:
                            saveAsExcel(exportDialog.FileName);
                            break;

                        default:
                            // shouldn't happen
                            throw new Exception(&quot;Specified export format not supported.&quot;);
                    }
                });
            }
            catch (System.IO.IOException ex)
            {
                 statusBar.text(&quot;Export failed&quot;);
                 logger.logError(&quot;Export failed&quot; + ex.Message + &quot;\n&quot; + ex.StackTrace);

                 return;
            }
        }
    }

    private delegate void updateProgressDelegate(int percentage);

    public void updateProgress(int percentage)
    {
        if (statusBar.InvokeRequired)
        {
            var d = updateProgressDelegate(updateProgress);
            statusBar.Invoke(d, percentage);
        }
        else
        {
            _updateProgress(percentage);
        }
    }

    private void saveAsTxt(string filename)
    {
        IProgress&lt;int&gt; progress = new Progress&lt;int&gt;(updateProgress);
        
        // save the text file, while reporting progress....
    }

    private void saveAsCsv(string filename)
    {
        IProgress&lt;int&gt; progress = new Progress&lt;int&gt;(updateProgress);
        
        using (StreamWriter writer = StreamWriter(filename))
        {
            // write the headers and the data, while reporting progres...
        }
    }

    private void saveAsExcel(string filename)
    {
        IProgress&lt;int&gt; progress = Progress&lt;int&gt;(updateProgress);

        // EPPlus magic to write the data, while reporting progress...
    }
}

Questions
How can this be refactored to make it more extensible? That is, if I wanted to add support for more file types, make it easy and quicker to modify. The switch statement could get very long. Essentially, how to comply with the Open/Closed principle?
I would suggest moving the actual export(s) into their own class.  We can create an interface for exports.  Something along the lines of
public interface IExport&lt;T&gt;
{
    Task SaveAsync(string fileName, IEnumerable&lt;T&gt; items, IProgress&lt;int&gt; progress = null);
    string ExportType { get; }
}

Then each export type can implement this interface.
public class ExportItemsToText : IExport&lt;Item&gt;
{
    public Task SaveAsync(string fileName, IEnumerable&lt;Item&gt; items, IProgress&lt;int&gt; progress = null)
    {
        throw new NotImplementedException();
    }

    public string ExportType =&gt; &quot;txt&quot;;
}

Then in your constructor of ExportWindow
public ExportWindow(IEnumerable&lt;IExport&lt;Item&gt;&gt; exports)
{
    // if using DI otherwise could just fill in dictionary here
    ExportStrategy = exports.ToDictionary(x =&gt; x.ExportType, x =&gt; x);
}

Instead of a switch statement you can now just look up the key in the dictionary to find what export should be ran and if not found would be the same as your default case.
IExport&lt;Item&gt; exporter;
if (ExportStrategy.TryGetValue(ext.ToLower(), out exporter))
{
    await exporter.SaveAsync(exportDialog.FileName, items, new Progress&lt;int&gt;(updateProgress))
}
else
{
    throw new Exception(&quot;Specified export format not supported.&quot;);
}

Now in the future if adding support for more types you just implement the interface and update your DI container.  Or if not using DI then would need to add it to the constructor of your ExportWindow.
I don't think this is a great idea but If you really don't want to create a class per export, which I think you should, you could make the dictionary IDictionary&lt;string, Action&lt;string&gt;&gt; then just put your methods in there and when adding a new type create the method and update the dictionary.