การส่งออกวัตถุในรูปแบบต่างๆขณะรายงานความคืบหน้า
คำอธิบาย
แอปพลิเคชัน 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 อาจใช้เวลานานมาก โดยพื้นฐานแล้วจะปฏิบัติตามหลักการเปิด / ปิดอย่างไร?
คำตอบ
ฉันขอแนะนำให้ย้ายการส่งออกจริงไปยังชั้นเรียนของตนเอง เราสามารถสร้างอินเทอร์เฟซสำหรับการส่งออก บางสิ่งบางอย่างตามแนวของ
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>>
จากนั้นใส่วิธีการของคุณไว้ที่นั่นและเมื่อเพิ่มประเภทใหม่ให้สร้าง วิธีการและอัปเดตพจนานุกรม
ฉันแค่ต้องการแบ่งปันสิ่งที่ฉันมีตั้งแต่ฉันได้นำสิ่งนี้ไปใช้ (ประเภท) ในหนึ่งในโครงการก่อนหน้าของฉัน (มันอยู่บน 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
กับคลาสนั้น สุดท้ายอัปเดตคลาสคอนเทนเนอร์ด้วยประเภทใหม่ (เพิ่มวิธีการเพื่อเปิดอินสแตนซ์ใหม่ของวิธีนี้) และคุณก็พร้อมแล้ว