Home > Back-end >  CSVHelper generic component in Blazor - How to pass in Type Map
CSVHelper generic component in Blazor - How to pass in Type Map

Time:01-01

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.

  • Related