1

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();
    }
 
}

bph
  • 59
  • 4
  • According to [this answer](https://stackoverflow.com/a/68864959) by Craig Brown, as of .NET 6 you can use generic constraints in Blazor, so I think you could define a second generic parameter `@typeparam TMap where TMap : CsvHelper.Configuration.ClassMap`, then use `csv1.Context.RegisterClassMap()`. – dbc Dec 31 '22 at 19:33
  • Does that do what you need? I don't have Blazor to test. – dbc Dec 31 '22 at 19:38
  • Don't really understand what the U type is you are referring to? You're mapping or something else? My guess is the component should be only dependent on the T type you are showing in your grid, and the export code pushed outside of the view into an export service. – Filip Cordas Dec 31 '22 at 19:50
  • @FilipCordas - `U` is a CsvHelper [`ClassMap`](https://joshclose.github.io/CsvHelper/examples/configuration/class-maps/) that defines how to map the type `T` to the CSV file. Unlike with (say) `XmlSerializer`, with CsvHelper one typically separates the mapping and data model into different classes. – dbc Dec 31 '22 at 19:53
  • But as I mentioned the mapping class should be generic in regard to T Map and you can register it as such in your DI container and resolve it as such if it implements the class. class CustomTMapping:Map can be registered as Map that you [Inject] into the class. As I said best to register an Export service and not mix your Business Logic with your views. – Filip Cordas Dec 31 '22 at 20:02
  • @dbc Yes... That's perfect. I don't know why I didn't get that before. (I would accept your comment as the answer if it were an answer, not a comment.) Thank you!! – bph Dec 31 '22 at 23:08
  • @bph - should I make that an answer then? – dbc Dec 31 '22 at 23:10
  • @dbc If you wish. – bph Dec 31 '22 at 23:13

2 Answers2

0

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.

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • Thank you. I don't understand what a "constraint" on a type really is, but I can try to read up on it. Thank you. Ideally, I'd like to make the second type an optional parameter. – bph Dec 31 '22 at 23:10
  • There's no such thing as an optional generics. You'll need two components, one inhering from the other. – MrC aka Shaun Curtis Jan 01 '23 at 00:32
0

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();
    } 
}
dbc
  • 104,963
  • 20
  • 228
  • 340