1

I'm using the Language-Ext library to create a generic union type that represents one of three states...

  [Union]
  public interface LoadingOption<T> {
    LoadingOption<T> Loading();
    LoadingOption<T> Loaded(T value);
    LoadingOption<T> NotLoaded();
  }

I then have a Blazor component that will show one of three render fragments, depending on which of the three states is represented by the instance of the union...

@typeparam T

@(Data switch {
  Loading<T> => Loading,
  Loaded<T> t => Loaded(t.Value),
  NotLoaded<T> => NotLoaded,
  _ => Loading
  })

@code {

  [Parameter]
  public LoadingOption<T> Data { get; set; }

  [Parameter]
  public RenderFragment<Loading<T>> Loading { get; set; }

  [Parameter]
  public RenderFragment<Loaded<T>> Loaded { get; set; }

  [Parameter]
  public RenderFragment<NotLoaded<T>> NotLoaded { get; set; }

}

Note that the default case in the switch statement is needed to prevent an exception Non-exhaustive switch expression failed to match its input when the page firsts loads.

However, when I try to use this component, the HTML is never rendered. Instead, I see Microsoft.AspNetCore.Components.RenderFragment on the page, which sounds like it's treating the RenderFragment as a plain object, rather than as a RenderFragment

Sample usage of the Loader.razor component is as follows...

@page "/LoadingOptionSample"
<h3>Loader sample</h3>

<p><button class="btn btn-primary" @onclick="LoadCustomerFound">Existing customer</button>
   <button class="btn btn-secondary" @onclick="LoadCustomerNotFound">No such customer</button></p>

<Loader Data="_customer" Context="customer">
  <Loading>
    <p>Please wait while we look for the customer...</p>
  </Loading>
  <Loaded>
    <p>Found @customer.Value.Name (id: @customer.Value.Id), who is @customer.Value.Age years old</p>
  </Loaded>
  <NotLoaded>
    <p>Big flop, we can't find him</p>
  </NotLoaded>
</Loader>

@code {

  private LoadingOption<Customer> _customer;

  private async Task LoadCustomerFound() {
    _customer = new Loading<Customer>();
    // Simulate database access so we can see the Loading HTML...
    await Task.Delay(2000);
    _customer = new Loaded<Customer>(new Customer(1, "Jim Spriggs", 42));
  }

  private async Task LoadCustomerNotFound() {
    _customer = new Loading<Customer>();
    await Task.Delay(2000);
    _customer = new NotLoaded<Customer>();
  }

}

After clicking one of the buttons, the page looks like this...

Sample output

Doesn't matter which of the buttons I click, the output is the same, and doesn't change. This supports my suspicion that Blazor isn't treating the RenderFragment as such, but treats it as a plain .NET object and renders it by calling ToString(), which is why I see the type, not the content.

If I do a non-generic version of this, it works fine.

Note that I have repeatedly restarted VS, as well as trying this in a separate project, but the result is always the same, so it seems the problem is encapsulated in the three files shown above.

Anyone any idea what I'm doing wrong? Thanks

Update I tried using a regular switch statement, instead of the switch expression, but it didn't make any difference. However, while testing this, I discovered that if I didn't have the switch at all, and simply did this...

  @NotLoaded

...then I still got the type shown on the page.

Avrohom Yisroel
  • 8,555
  • 8
  • 50
  • 106
  • There is mismatch between that `[union]` thing and how switch expressions work. Your return values are not all of the same type. Try a traditional switch statement. – H H Jun 27 '21 at 21:15
  • @henk Thanks for the suggestion, but it didn't make any difference. Please see my updated question, as your suggestion led to a discovery that may be significant. Thanks again. – Avrohom Yisroel Jun 28 '21 at 13:29
  • You are missing a parameter. It should be something like `@NotLoaded(Data)` , although that might not compile. – H H Jun 28 '21 at 20:32
  • @HenkHolterman `NotLoaded` and `Loading` don't take a parameter, or at least they shouldn't. Only `Loaded` takes a parameter, namely the entity that has been loaded. If you look at the `[Union]` code, you can see this. Not sure if I got the `RenderFragment` declarations right in the Blazor component though. Maybe that's where it's going wrong. However, if that's the problem, I would have expected either `NotLoaded` and `Loading` to work, or `Loaded` to work. As it is, all three just show the `RenderFragment`'s type, as opposed to rendering it as I want. Thanks again, any more ideas? – Avrohom Yisroel Jun 28 '21 at 21:06
  • Is there a reason why you can't use a generic "Loading" component with three states and three RenderFragments? Then set the state from the parent? – MrC aka Shaun Curtis Jun 28 '21 at 21:15
  • @MrCakaShaunCurtis Not sure what you mean by "_set the state from the parent._" The point of this component is to be able to pass in the entity to be displayed as a parameter, and have the component sort out what to display, based on the state of the entity (or rather the state containing it). Please could you clarify what you mean? Thanks for the reply. – Avrohom Yisroel Jun 28 '21 at 21:21
  • Loading/Loaded suggests some async data loading is taking place in the component lifecycle of the parent. This component sets a Parameter on the child `Loading` component to reflect the state of the loading process, and therefore which RenderFragment `Loading` shows. I can post some code in an answer tomorrow if you wish to demo. – MrC aka Shaun Curtis Jun 28 '21 at 21:31
  • @MrCakaShaunCurtis Yes, this is for async loading. Maybe if you don't mind posting some code it would be easier to see what you mean. Thanks – Avrohom Yisroel Jun 29 '21 at 12:46

1 Answers1

1

This answer demonstrates a somewhat different approach to managing component loading and state.

Enum for the State

    public enum DataLoaderState { NotSet, Loading, Loaded, Error }

A basic Loader Component.

@namespace Blazor.Starter.Components

@inherits ComponentBase
@if (this.State == DataLoaderState.Loading)
{
    if (this.LoadingContent != null)
    {
        @this.LoadingContent
    }
    else
    {
        <div class="m-2 p-2">Loading......</div>
    }
}
else if (this.State == DataLoaderState.Error)
{
    if (this.ErrorContent != null)
    {
        @this.ErrorContent
    }
    else
    {
        <div class="m-2 p-2">Error Loading Data</div>
    }
}
else
{
    @this.ChildContent
}
@code{

    [Parameter] public RenderFragment ChildContent { get; set; }
    [Parameter] public RenderFragment LoadingContent { get; set; }
    [Parameter] public RenderFragment ErrorContent { get; set; }
    [Parameter] public DataLoaderState State { get; set; } = DataLoaderState.NotSet;
}

A sample page similar to the one in the question to demo the Loader.

@page "/"

<h2>Home Page</h2>

<DataLoader State="loadState">
    <LoadingContent>
        <div class="m-2 p-2 bg-warning">
            Please wait while we look for the customer...
        </div>
    </LoadingContent>
    <ChildContent>
        <div class="m-2 p-2 bg-success">
            Name: @Model.Name - Age: @Model.Age
        </div>
    </ChildContent>
    <ErrorContent>
        <div class="m-2 p-2 bg-danger">
            Big flop, we can't find him
        </div>
    </ErrorContent>
</DataLoader>

<DataLoader State="loadState">
    <div class="m-2 p-2">
        Loaded
    </div>
</DataLoader>

<div class="m-2 p-2">
    <button class="btn btn-primary" @onclick="LoadAsync">Reload</button>
</div>

<div class="m-2 p-2">
    <button class="btn btn-danger" @onclick="LoadErrorAsync">Reload with Error</button>
</div>

@code {

    private DataLoaderState loadState = DataLoaderState.Loaded;

    private Customer Model;

    protected async override Task OnInitializedAsync()
    {
        await LoadAsync();
    }

    private async Task LoadAsync()
    {
        Model = null;
        loadState = DataLoaderState.Loading;
        // emulate a slow async data load
        await Task.Delay(3000);
        Model = new Customer { Name = "Billy Bloggs", Age = 24 };
        loadState = DataLoaderState.Loaded;
    }

    private async Task LoadErrorAsync()
    {
        Model = null;
        loadState = DataLoaderState.Loading;
        // emulate a slow async data load
        await Task.Delay(3000);
        loadState = DataLoaderState.Error;
    }

    public class Customer
    {
        public Guid ID { get; set; } = Guid.NewGuid();
        public string Name { get; set; }
        public int Age { get; set; }
    }
}
MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • Thanks for the reply. This is similar in a way to what I'm currently doing (see https://github.com/MrYossu/Pixata.Utilities/blob/master/Pixata.Blazor.LanguageExtComponents/Components/LoadingOption.razor for the component and https://github.com/MrYossu/Pixata.Utilities/blob/master/Pixata.Blazor.Sample/Pages/LoadingOptionSample.razor for a sample of how to use it). It works, but I can't help but feel that I ought to be able to do this with just the one parameter to the component, not two. Thanks again. – Avrohom Yisroel Jun 30 '21 at 13:15