Exporter des objets dans différents formats tout en rapportant la progression

Aug 18 2020

La description

Une application WinForms a une fonction pour exporter des objets du type suivant, dans différents formats:

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

Au clic d'un bouton dans une fenêtre, un SaveFileDialogest affiché, et actuellement il offre la possibilité d'enregistrer les données au format .txt, .csv ou .xlsx. Comme il y a parfois des centaines ou des milliers d'objets et que l'interface utilisateur ne doit pas se figer, a Taskest utilisé pour exécuter cette opération. Cette implémentation fonctionne, mais pourrait être améliorée.

Code

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...
    }
}

Des questions

Comment cela peut-il être remanié pour le rendre plus extensible? Autrement dit, si je voulais ajouter la prise en charge de plus de types de fichiers, rendez la modification facile et plus rapide. L'instruction switch peut être très longue. Essentiellement, comment respecter le principe Ouvert / Fermé?

Réponses

5 CharlesNRice Aug 18 2020 at 20:04

Je suggérerais de déplacer les exportations réelles dans leur propre classe. Nous pouvons créer une interface pour les exportations. Quelque chose du genre

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

Ensuite, chaque type d'export peut implémenter cette interface.

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

    public string ExportType => "txt";
}

Puis dans votre constructeur d'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);
}

Au lieu d'une instruction switch, vous pouvez maintenant simplement rechercher la clé dans le dictionnaire pour trouver quelle exportation doit être exécutée et si elle n'est pas trouvée, elle correspond à votre cas par défaut.

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.");
}

À l'avenir, si vous ajoutez la prise en charge de plusieurs types, vous implémentez simplement l'interface et mettez à jour votre conteneur DI. Ou si vous n'utilisez pas DI, vous devrez l'ajouter au constructeur de votre ExportWindow.

Je ne pense pas que ce soit une bonne idée, mais si vous ne voulez vraiment pas créer une classe par exportation, ce que je pense que vous devriez, vous pouvez créer le dictionnaire, IDictionary<string, Action<string>>puis simplement mettre vos méthodes là-dedans et lors de l'ajout d'un nouveau type créer la méthode et mettre à jour le dictionnaire.

2 iSR5 Aug 21 2020 at 06:22

Je veux juste partager ce que j'ai depuis que j'ai déjà implémenté cela (en quelque sorte) dans l'un de mes projets précédents (c'était sur ASP.NET), mais cela peut être appliqué dans n'importe quel autre environnement. La mise en œuvre était similaire à la suggestion de CharlesNRice. Cependant, l'exigence était de n'avoir que des options pour exporter les rapports système (qui n'est utilisé qu'un seul modèle de rapport) vers Pdf, Excel et Word avec une négociation pour avoir plus d'options d'exportation à l'avenir. Alors voici comment je l'ai fait:

D'abord l'interface:

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

    void Download(string fileName);

    void SaveAs(string fileFullPath);
}

puis la classe de conteneur:

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
}

J'ai implémenté une classe pour chaque type d'exportation comme nous pouvons le voir dans la classe ci-dessus. Je vais partager une classe (je vais la simplifier à partir de la classe réelle).

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
}

Downloadet SaveAssont différents (pas les mêmes). Downloadtéléchargerait le fichier exporté, tout en SaveAsenregistrant l'instance d'objet. Mais cela a été implémenté comme ça parce que les dépendances utilisées.

Maintenant, l'utilisation aimerait ceci:

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

C'est ainsi que j'ai mis en œuvre ce projet, cela pourrait être amélioré, mais pour les besoins de l'entreprise, cela suffit.

Chaque fois que vous avez besoin d'ajouter un nouveau type d'exportation, créez simplement une nouvelle sealedclasse, puis implémentez-la IExportTo<T>, IDisposablesur cette classe. Enfin, mettez à jour la classe de conteneur avec le nouveau type (ajoutez une méthode pour ouvrir une nouvelle instance de cette méthode) et vous êtes prêt à partir.