Esportazione di oggetti in vari formati durante la segnalazione dello stato di avanzamento

Aug 18 2020

Descrizione

Un'applicazione WinForms ha una funzione per esportare oggetti del seguente tipo, in vari formati:

class Item
{
    public int id { get; set; }
    public string description { get; set; }
}

Facendo clic su un pulsante in una finestra, SaveFileDialogviene visualizzato un, che attualmente offre la possibilità di salvare i dati in formato .txt, .csv o .xlsx. Poiché a volte ci sono centinaia o migliaia di oggetti e l'interfaccia utente non dovrebbe bloccarsi, Taskper eseguire questa operazione viene utilizzato un . Questa implementazione funziona, ma potrebbe essere migliorata.

Codice

public partial class ExportWindow : Form
{
    // objects to be exported
    List<Item> items;

    // event handler for the "Export" button click
    private async void exportButton_click(object sender, System.EventArgs e)
    {
        SaveFileDialog exportDialog = new SaveFileDialog();
        exportDialog.Filter = "Text File (*.txt)|*.txt|Comma-separated values file (*.csv)|*.csv|Excel spreadsheet (*.xlsx)|*.xlsx";
        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("Exporting");

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

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

                        default:
                            // shouldn't happen
                            throw new Exception("Specified export format not supported.");
                    }
                });
            }
            catch (System.IO.IOException ex)
            {
                 statusBar.text("Export failed");
                 logger.logError("Export failed" + ex.Message + "\n" + 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<int> progress = new Progress<int>(updateProgress);
        
        // save the text file, while reporting progress....
    }

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

    private void saveAsExcel(string filename)
    {
        IProgress<int> progress = Progress<int>(updateProgress);

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

Domande

Come può essere modificato per renderlo più estensibile? Cioè, se volessi aggiungere il supporto per più tipi di file, renderlo più facile e veloce da modificare. L'istruzione switch potrebbe diventare molto lunga. In sostanza, come rispettare il principio Aperto / Chiuso?

Risposte

5 CharlesNRice Aug 18 2020 at 20:04

Suggerirei di spostare le esportazioni effettive nella loro classe. Possiamo creare un'interfaccia per le esportazioni. Qualcosa sulla falsariga di

public interface IExport<T>
{
    Task SaveAsync(string fileName, IEnumerable<T> items, IProgress<int> progress = null);
    string ExportType { get; }
}

Quindi ogni tipo di esportazione può implementare questa interfaccia.

public class ExportItemsToText : IExport<Item>
{
    public Task SaveAsync(string fileName, IEnumerable<Item> items, IProgress<int> progress = null)
    {
        throw new NotImplementedException();
    }

    public string ExportType => "txt";
}

Quindi nel tuo costruttore di ExportWindow

public ExportWindow(IEnumerable<IExport<Item>> exports)
{
    // if using DI otherwise could just fill in dictionary here
    ExportStrategy = exports.ToDictionary(x => x.ExportType, x => x);
}

Invece di un'istruzione switch puoi ora cercare la chiave nel dizionario per trovare quale esportazione dovrebbe essere eseguita e se non trovata sarebbe la stessa del tuo caso predefinito.

IExport<Item> exporter;
if (ExportStrategy.TryGetValue(ext.ToLower(), out exporter))
{
    await exporter.SaveAsync(exportDialog.FileName, items, new Progress<int>(updateProgress))
}
else
{
    throw new Exception("Specified export format not supported.");
}

Ora, in futuro, se si aggiunge il supporto per più tipi, è sufficiente implementare l'interfaccia e aggiornare il contenitore DI. Oppure, se non si utilizza DI, è necessario aggiungerlo al costruttore di ExportWindow.

Non penso che questa sia una grande idea ma se davvero non vuoi creare una classe per esportazione, cosa che penso dovresti, potresti creare il dizionario, IDictionary<string, Action<string>>quindi inserire i tuoi metodi lì e quando aggiungi un nuovo tipo crea il metodo e aggiorna il dizionario.

2 iSR5 Aug 21 2020 at 06:22

Voglio solo condividere ciò che ho poiché l'ho già implementato (una specie di) in uno dei miei progetti precedenti (era su ASP.NET), ma può essere applicato in qualsiasi altro ambiente. L'implementazione era simile al suggerimento di CharlesNRice. Tuttavia, il requisito era di avere solo le opzioni per esportare i rapporti di sistema (che viene utilizzato solo un modello di rapporto) in Pdf, Excel e Word con la negoziazione di avere più opzioni di esportazione in futuro. Quindi è così che l'ho fatto:

Prima l'interfaccia:

public interface IExportTo<T>
{
    IExportTo<T> Generate();

    void Download(string fileName);

    void SaveAs(string fileFullPath);
}

quindi la classe contenitore:

public class ExportTo : IDisposable
{
    private readonly IList<T> _source;

    public ExportTo(IList<T> source)
    {
        _source = source;
    }

    public ExportExcel Excel()
    {
        return new ExportExcel(_source);
    }

    public ExportPdf Pdf()
    {
        return new ExportPdf(_source);
    }
    
    public ExportWord Word()
    {
        return new ExportPdf(_source);
    }
    

    #region IDisposable

    private bool _disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                Dispose();
            }

            _disposed = true;
        }
    }


    ~ExportTo()
    {
        Dispose(false);
    }

    #endregion
}

Ho implementato una classe per ogni tipo di esportazione come possiamo vedere nella classe sopra. Condividerò una classe (la semplificherò dalla classe attuale).

public sealed class ExportPdf : IExportTo<T>, IDisposable
{
    private readonly IList<T> _source;

    private ExportPdf() { }

    public ExportPdf(IList<T> source) : this() => _source = source ?? throw new ArgumentNullException(nameof(source));

    public IExportTo<T> Generate()
    {
        // some implementation 
        return this;
    }

    // another overload to generate by Id 
    public IExportTo<T> Generate(long reportId)
    {
        // do some work 
        return this;
    }

    // Download report as file 
    public void Download(string fileName)
    {
       // do some work 
    }

    public void SaveAs(string fileFullPath)
    {
        throw new NotImplementedException("This function has not been implemented yet. Only download is available for now.");
    }


    #region IDisposable

    private bool _disposed = false;

    public void Dispose()
    {   
        Dispose(true);
        GC.SuppressFinalize(this);
    }


    private void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                Dispose();
            }

            _disposed = true;
        }
    }


    ~ExportPdf()
    {
        Dispose(false);
    }

    #endregion
}

Downloade SaveAssono diversi (non uguali). Downloadscaricherà il file esportato, mentre SaveAssalverà l'istanza dell'oggetto. Ma questo è stato implementato in questo modo perché le dipendenze utilizzate.

Ora l'utilizzo vorrebbe questo:

new ExportTo(someList)
.Pdf()
.Generate()
.Download(fileName);

È così che l'ho implementato in quel progetto, potrebbe essere migliorato, ma per esigenze aziendali è sufficiente.

Ogni volta che devi aggiungere un nuovo tipo di esportazione, crea semplicemente una nuova sealedclasse, quindi implementala IExportTo<T>, IDisposablesu quella classe. Infine, aggiorna la classe contenitore con il nuovo tipo (aggiungi un metodo per aprire una nuova istanza di questo metodo) e sei a posto.