Mengekspor objek dalam berbagai format sambil melaporkan kemajuan

Aug 18 2020

Deskripsi

Aplikasi WinForms memiliki fungsi untuk mengekspor objek dengan tipe berikut, dalam berbagai format:

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

Dengan mengklik tombol di jendela, a SaveFileDialogditampilkan, dan saat ini memberikan opsi untuk menyimpan data dalam format .txt, .csv atau .xlsx. Karena terkadang ada ratusan atau ribuan objek, dan UI tidak boleh berhenti, a Taskdigunakan untuk menjalankan operasi ini. Implementasi ini berhasil, tetapi dapat ditingkatkan.

Kode

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

Pertanyaan

Bagaimana ini bisa direfraktorisasi agar lebih bisa diperluas? Artinya, jika saya ingin menambahkan dukungan untuk lebih banyak jenis file, buat mudah dan cepat untuk memodifikasi. Pernyataan switch bisa sangat panjang. Intinya, bagaimana cara mematuhi prinsip Terbuka / Tertutup?

Jawaban

5 CharlesNRice Aug 18 2020 at 20:04

Saya sarankan untuk memindahkan ekspor yang sebenarnya ke kelas mereka sendiri. Kami dapat membuat antarmuka untuk ekspor. Sesuatu di sepanjang garis

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

Kemudian setiap jenis ekspor dapat mengimplementasikan antarmuka ini.

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

    public string ExportType => "txt";
}

Kemudian di konstruktor ExportWindow Anda

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

Alih-alih pernyataan switch, Anda sekarang dapat mencari kunci di kamus untuk menemukan ekspor apa yang harus dijalankan dan jika tidak ditemukan akan sama dengan kasus default Anda.

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

Sekarang di masa depan jika menambahkan dukungan untuk lebih banyak tipe Anda cukup mengimplementasikan antarmuka dan memperbarui wadah DI Anda. Atau jika tidak menggunakan DI maka perlu menambahkannya ke konstruktor ExportWindow Anda.

Menurut saya ini bukan ide yang bagus, tetapi Jika Anda benar-benar tidak ingin membuat kelas per ekspor, yang menurut saya sebaiknya Anda lakukan, Anda dapat membuat kamus IDictionary<string, Action<string>>lalu meletakkan metode Anda di sana dan saat menambahkan tipe baru buat metode dan perbarui kamus.

2 iSR5 Aug 21 2020 at 06:22

Saya hanya ingin berbagi apa yang saya miliki karena saya telah menerapkan ini (semacam) di salah satu proyek saya sebelumnya (itu di ASP.NET), tetapi dapat diterapkan di lingkungan lain. Implementasinya serupa dengan saran CharlesNRice. Namun, persyaratannya adalah hanya memiliki opsi untuk mengekspor laporan sistem (yang hanya menggunakan satu templat laporan) ke Pdf, Excel, dan Word dengan negosiasi untuk memiliki lebih banyak opsi ekspor di masa mendatang. Jadi begini cara saya melakukannya:

Pertama antarmuka:

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

    void Download(string fileName);

    void SaveAs(string fileFullPath);
}

lalu kelas kontainer:

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
}

Saya telah menerapkan kelas untuk setiap jenis ekspor seperti yang dapat kita lihat di kelas di atas. Saya akan berbagi satu kelas (saya akan menyederhanakannya dari kelas yang sebenarnya).

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
}

Downloaddan SaveAsberbeda (tidak sama). Downloadakan mengunduh file yang diekspor, sementara SaveAsakan menyimpan instance objek. Tapi ini diimplementasikan seperti ini karena dependensi yang digunakan.

Sekarang penggunaannya seperti ini:

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

Beginilah cara saya menerapkan dalam proyek itu, itu bisa ditingkatkan, tetapi untuk persyaratan bisnis itu sudah cukup.

Kapan pun Anda perlu menambahkan jenis ekspor baru, cukup buat sealedkelas baru , lalu terapkan IExportTo<T>, IDisposabledi kelas itu. Terakhir, perbarui kelas kontainer dengan tipe baru (tambahkan metode untuk membuka contoh baru dari metode ini) dan Anda siap melakukannya.