Search code examples
c#genericsblazorblazor-server-sidecsvhelper

CSVHelper generic component in Blazor - How to pass in Type Map


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


Solution

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