We are creating a LOB application/site using server-side Blazor under .Net 7. On every list, we have a button to download a CSV. Things work pretty well for a one-off, but it's a lot of duplicated code, and a change has to be made everywhere. Enter... (ta-da...) generic components!
I wrapped everything I need into a component, and my download button works as intended. Each thing we are downloading has a POCO/EF class defined. I can pass that class to my component as a TypeParm (<T>)
.
My problem is that most of those classes have a matching Map class. There is something I don't totally understand (generics are not my strong suit to begin with), but a Blazor component has trouble with multiple generic types.
If it's possible to pass in a <T>
and a, say <U>
, I can't figure out how to do it. There are a few articles about partial classes and generics, and there may have been some bugs in early Blazor/razor versions.
I can pass in the NAME of the type class map as a string, but I can't figure out how to pass the class or how go get the string of a TypeName into being the class itself to call RegisterClassMap
.
I would be very appreciative if someone could share the magic sauce or tell me where to go for more info.
In reviewing the suggestions (before posting), I saw nothing exactly like this, but I wonder if the approach would be to have a super-class:
public class TypeForCSV
{
object TheListClass,
object TheMapClass
}
But I (personally) would still be stuck on how to define that to make it generic.
Thank you all.
Here's my component. The comment line is my issue. (Yes, we also use MudBlazor.)
@using BlazorDownloadFile
@typeparam T
<MudButton Variant="@(_processing ? Variant.Filled : Variant.Outlined )"
DisableElevation="true"
Size="Size.Small"
Color="Color.Primary"
StartIcon="@(_processing ? "" : Icons.Filled.BrowserUpdated)"
OnClick="@ExportToCSV"
Disabled="@_processing"
Class="ma-2 mt-8">
@if (_processing)
{
<MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true" />
<span> Preparing...</span>
}
else
{
<span>Export CSV</span>
}
</MudButton>
@code {
private bool _processing = false;
[Inject] public IBlazorDownloadFileService? BlazorDownloadFileService { get; set; }
[Parameter] public string FileNameBase { get; set; } = "";
[Parameter] public string CsvMapType { get; set; } = "NothingToSeeHere";
[Parameter] public Func<Task<List<T>>>? OnListRequest { get; set; }
public List<T> ListItems { get; set; } = new();
private string _csv = "";
private string MakeCsvString(List<T> items)
{
using (var writer = new StringWriter())
using (var csv1 = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
// csv1.Context.RegisterClassMap<U>(); // use a different overload?
csv1.WriteRecords(items);
return writer.ToString();
}
}
private async Task ExportToCSV()
{
_processing = true;
StateHasChanged();
ListItems = await OnListRequest();
_csv = MakeCsvString(ListItems);
string filename = $"{FileNameBase.IfNullOrWhiteSpace("Download")}-{DateTime.Now:yyyyMMdd-HHmm}.csv";
await BlazorDownloadFileService.DownloadFileFromText(filename, _csv, System.Text.Encoding.UTF8, "text/csv");
await Task.Yield();
_processing = false;
StateHasChanged();
}
}
CodePudding user response:
According to this answer by Craig Brown to Are generic type constraints possible in blazor?, as of .NET 6 you can use generic constraints in Blazor. Since you are using .NET 7, you could define a second generic parameter and constrain it to be a ClassMap<T>
as follows:
@typeparam TMap where TMap : CsvHelper.Configuration.ClassMap<T>
So your full code might look like:
@using BlazorDownloadFile
@typeparam T
@typeparam TMap where TMap : CsvHelper.Configuration.ClassMap<T>
<MudButton Variant="@(_processing ? Variant.Filled : Variant.Outlined )"
DisableElevation="true"
Size="Size.Small"
Color="Color.Primary"
StartIcon="@(_processing ? "" : Icons.Filled.BrowserUpdated)"
OnClick="@ExportToCSV"
Disabled="@_processing"
Class="ma-2 mt-8">
@if (_processing)
{
<MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true" />
<span> Preparing...</span>
}
else
{
<span>Export CSV</span>
}
</MudButton>
@code {
private bool _processing = false;
[Inject] public IBlazorDownloadFileService? BlazorDownloadFileService { get; set; }
[Parameter] public string FileNameBase { get; set; } = "";
[Parameter] public Func<Task<List<T>>>? OnListRequest { get; set; }
public List<T> ListItems { get; set; } = new();
private string _csv = "";
private string MakeCsvString(List<T> items)
{
using (var writer = new StringWriter())
using (var csv1 = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv1.Context.RegisterClassMap<TMap>();
csv1.WriteRecords(items);
return writer.ToString();
}
}
private async Task ExportToCSV()
{
_processing = true;
StateHasChanged();
ListItems = await OnListRequest();
_csv = MakeCsvString(ListItems);
string filename = $"{FileNameBase.IfNullOrWhiteSpace("Download")}-{DateTime.Now:yyyyMMdd-HHmm}.csv";
await BlazorDownloadFileService.DownloadFileFromText(filename, _csv, System.Text.Encoding.UTF8, "text/csv");
await Task.Yield();
_processing = false;
StateHasChanged();
}
}
CodePudding user response:
You can pass in as many generic types are you like with restraints on each.
Component:
@typeparam TRecord where TRecord: class, new()
@typeparam TService where TService: class, IService<TRecord>
@typeparam TBase where TBase: class, IDisposable
<h3>Component</h3>
@code {
}
And:
@page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<Component TBase=TestBaseClass TRecord=TestTRecord TService=TestService />
@code {
}
I would suggest using interfaces to qualify what classes can be passed and using descriptive names for the generic types rather that T
and U
to make the code more readable.