İlerlemeyi bildirirken nesneleri çeşitli formatlarda dışa aktarma
Açıklama
Bir WinForms uygulamasının aşağıdaki türdeki nesneleri çeşitli biçimlerde dışa aktarma işlevi vardır:
class Item
{
public int id { get; set; }
public string description { get; set; }
}
Bir penceredeki bir düğmeye tıklandığında, a SaveFileDialog
gösterilir ve şu anda verileri .txt, .csv veya .xlsx biçiminde kaydetme seçeneği sunar. Bazen yüzlerce veya binlerce nesne olduğu ve kullanıcı arayüzünün donmaması gerektiğinden, Task
bu işlemi çalıştırmak için a kullanılır. Bu uygulama işe yarıyor, ancak geliştirilebilir.
Kod
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...
}
}
Sorular
Daha genişletilebilir hale getirmek için bu nasıl yeniden düzenlenebilir? Diğer bir deyişle, daha fazla dosya türü için destek eklemek istersem, değiştirmeyi kolaylaştırın ve hızlandırın. Switch ifadesi çok uzun sürebilir. Esasen, Açık / Kapalı prensibine nasıl uyulur?
Yanıtlar
Gerçek ihracat (lar) ı kendi sınıflarına taşımayı öneririm. İhracat için bir arayüz oluşturabiliriz. Çizgisinde bir şey
public interface IExport<T>
{
Task SaveAsync(string fileName, IEnumerable<T> items, IProgress<int> progress = null);
string ExportType { get; }
}
Daha sonra her dışa aktarma türü bu arabirimi uygulayabilir.
public class ExportItemsToText : IExport<Item>
{
public Task SaveAsync(string fileName, IEnumerable<Item> items, IProgress<int> progress = null)
{
throw new NotImplementedException();
}
public string ExportType => "txt";
}
Ardından ExportWindow kurucunuzda
public ExportWindow(IEnumerable<IExport<Item>> exports)
{
// if using DI otherwise could just fill in dictionary here
ExportStrategy = exports.ToDictionary(x => x.ExportType, x => x);
}
Bir anahtar ifadesi yerine, artık hangi dışa aktarmanın çalıştırılması gerektiğini bulmak için sözlükte anahtara bakabilirsiniz ve bulunamazsa varsayılan durumunuzla aynı olacaktır.
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.");
}
Artık gelecekte daha fazla tür için destek eklerseniz, yalnızca arayüzü uygular ve DI kapsayıcınızı güncellersiniz. Veya DI kullanmıyorsanız, bunu ExportWindow'unuzun yapıcısına eklemeniz gerekir.
Bunun harika bir fikir olduğunu sanmıyorum ama eğer ihracat başına gerçekten bir sınıf oluşturmak istemiyorsan, ki bunu yapman gerektiğini düşünüyorum, sözlüğü yapabilir ve IDictionary<string, Action<string>>
sonra metotlarınızı oraya koyabilir ve yeni bir tür eklerken yöntemi ve sözlüğü güncelleyin.
Bunu daha önceki projelerimden birinde (ASP.NET'teydi) zaten uyguladığımdan beri sahip olduğum şeyleri paylaşmak istiyorum, ancak başka herhangi bir ortamda da uygulanabilir. Uygulama CharlesNRice önerisine benzerdi. Ancak, sistem raporlarını (yalnızca bir rapor şablonu kullanılan), gelecekte daha fazla dışa aktarma seçeneğine sahip olmanın müzakeresiyle Pdf, Excel ve Word'e dışa aktarma seçeneklerine sahip olmak gerekiyordu. İşte böyle yaptım:
Önce arayüz:
public interface IExportTo<T>
{
IExportTo<T> Generate();
void Download(string fileName);
void SaveAs(string fileFullPath);
}
ardından konteyner sınıfı:
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
}
Yukarıdaki sınıfta görebileceğimiz gibi, her ihracat türü için bir sınıf uyguladım. Bir sınıfı paylaşacağım (asıl dersten olsa da basitleştireceğim).
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
ve SaveAs
farklıdır (aynı değil). Download
dışa aktarılan dosyayı indirirken SaveAs
nesne örneğini kaydeder . Ancak bu, bağımlılıklar kullanıldığı için böyle uygulandı.
Şimdi kullanım şu şekilde olacaktır:
new ExportTo(someList)
.Pdf()
.Generate()
.Download(fileName);
Bu projede böyle uyguladım, geliştirilebilir, ancak iş gereksinimleri için yeterli.
Yeni bir dışa aktarma türü eklemeniz gerektiğinde, sadece yeni bir sealed
sınıf oluşturun, ardından IExportTo<T>, IDisposable
o sınıfta uygulayın. Son olarak, kapsayıcı sınıfını yeni türle güncelleyin (bu yöntemin yeni bir örneğini açmak için bir yöntem ekleyin) ve gitmeniz iyi olur.