Esportazione di oggetti in vari formati durante la segnalazione dello stato di avanzamento
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, SaveFileDialog
viene 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, Task
per 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
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.
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
}
Download
e SaveAs
sono diversi (non uguali). Download
scaricherà il file esportato, mentre SaveAs
salverà 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 sealed
classe, quindi implementala IExportTo<T>, IDisposable
su 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.