การส่งออกวัตถุในรูปแบบต่างๆขณะรายงานความคืบหน้า

Aug 18 2020

คำอธิบาย

แอปพลิเคชัน WinForms มีฟังก์ชันในการส่งออกวัตถุประเภทต่อไปนี้ในรูปแบบต่างๆ:

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

เมื่อคลิกปุ่มในหน้าต่าง a SaveFileDialogจะปรากฏขึ้นและปัจจุบันมีตัวเลือกในการบันทึกข้อมูลในรูปแบบ. txt, .csv หรือ. xlsx เนื่องจากบางครั้งมีวัตถุหลายร้อยหรือหลายพันชิ้นและ UI ไม่ควรหยุดทำงานTaskจึงใช้a เพื่อเรียกใช้การดำเนินการนี้ การใช้งานนี้ใช้งานได้ แต่สามารถปรับปรุงได้

รหัส

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

คำถาม

จะปรับโครงสร้างใหม่เพื่อให้สามารถขยายได้มากขึ้นได้อย่างไร? นั่นคือถ้าฉันต้องการเพิ่มการรองรับไฟล์ประเภทอื่น ๆ ให้แก้ไขได้ง่ายและเร็วขึ้น คำสั่ง switch อาจใช้เวลานานมาก โดยพื้นฐานแล้วจะปฏิบัติตามหลักการเปิด / ปิดอย่างไร?

คำตอบ

5 CharlesNRice Aug 18 2020 at 20:04

ฉันขอแนะนำให้ย้ายการส่งออกจริงไปยังชั้นเรียนของตนเอง เราสามารถสร้างอินเทอร์เฟซสำหรับการส่งออก บางสิ่งบางอย่างตามแนวของ

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

จากนั้นการส่งออกแต่ละประเภทสามารถใช้อินเทอร์เฟซนี้ได้

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

    public string ExportType => "txt";
}

จากนั้นในตัวสร้าง 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);
}

แทนที่จะเป็นคำสั่งสวิตช์คุณสามารถค้นหาคีย์ในพจนานุกรมเพื่อค้นหาสิ่งที่ควรรันการส่งออกและหากไม่พบจะเหมือนกับกรณีเริ่มต้นของคุณ

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

ในอนาคตหากเพิ่มการรองรับสำหรับประเภทอื่น ๆ คุณเพียงแค่ใช้อินเทอร์เฟซและอัปเดตคอนเทนเนอร์ DI ของคุณ หรือถ้าไม่ใช้ DI ก็ต้องเพิ่มลงในคอนสตรัคเตอร์ของ ExportWindow ของคุณ

ฉันไม่คิดว่านี่เป็นความคิดที่ดีแต่ถ้าคุณไม่ต้องการสร้างคลาสต่อการส่งออกซึ่งฉันคิดว่าคุณควรจะสร้างพจนานุกรมIDictionary<string, Action<string>>จากนั้นใส่วิธีการของคุณไว้ที่นั่นและเมื่อเพิ่มประเภทใหม่ให้สร้าง วิธีการและอัปเดตพจนานุกรม

2 iSR5 Aug 21 2020 at 06:22

ฉันแค่ต้องการแบ่งปันสิ่งที่ฉันมีตั้งแต่ฉันได้นำสิ่งนี้ไปใช้ (ประเภท) ในหนึ่งในโครงการก่อนหน้าของฉัน (มันอยู่บน ASP.NET) แต่สามารถนำไปใช้ในสภาพแวดล้อมอื่น ๆ ได้ การใช้งานคล้ายกับข้อเสนอแนะของ CharlesNRice อย่างไรก็ตามข้อกำหนดคือมีเพียงตัวเลือกในการส่งออกรายงานระบบ (ซึ่งใช้เพียงเทมเพลตรายงานเดียว) ไปยัง Pdf, Excel และ Word โดยมีการเจรจาเพื่อให้มีตัวเลือกการส่งออกเพิ่มเติมในอนาคต นี่คือวิธีที่ฉันทำ:

ขั้นแรกอินเทอร์เฟซ:

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

    void Download(string fileName);

    void SaveAs(string fileFullPath);
}

จากนั้นคลาสคอนเทนเนอร์:

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
}

ฉันได้ใช้คลาสสำหรับการส่งออกแต่ละประเภทดังที่เห็นในคลาสด้านบน ฉันจะแบ่งชั้นเรียนหนึ่งชั้น (ฉันจะทำให้ง่ายขึ้นแม้ว่าจะมาจากชั้นเรียนจริงก็ตาม)

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
}

DownloadและSaveAsแตกต่างกัน (ไม่เหมือนกัน) Downloadจะดาวน์โหลดไฟล์ที่ส่งออกในขณะที่SaveAsจะบันทึกอินสแตนซ์วัตถุ แต่สิ่งนี้ถูกนำมาใช้เช่นนี้เนื่องจากการอ้างอิงที่ใช้

ตอนนี้การใช้งานต้องการสิ่งนี้:

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

นี่คือวิธีที่ฉันดำเนินการในโครงการนั้นซึ่งสามารถปรับปรุงได้ แต่สำหรับความต้องการทางธุรกิจก็เพียงพอแล้ว

เมื่อใดก็ตามที่คุณต้องการเพิ่มการส่งออกประเภทใหม่เพียงแค่สร้างsealedคลาสใหม่จากนั้นนำไปใช้IExportTo<T>, IDisposableกับคลาสนั้น สุดท้ายอัปเดตคลาสคอนเทนเนอร์ด้วยประเภทใหม่ (เพิ่มวิธีการเพื่อเปิดอินสแตนซ์ใหม่ของวิธีนี้) และคุณก็พร้อมแล้ว