2
votes

I am attempting to create my first Razor Component in a Blazor Server-side project. The Razor Component is named MyComponent and has a property configured to retrieve its value from input:

MyComponent.razor

    [Parameter]
    public int Count {get; set;}

I am pulling the count from an injected service configured via IServiceCollection, which looks like this:

    public interface ICountingService
    {
        ValueTask<int> Get();
    }

The hosting page, Index.razor looks like the following:

@page "/"
@inject ICountingService Counter

<h1>Hello World!</h1>

<MyComponent Count="@Counter.Get()" />

However, I cannot seem to bind the correct value for the Count property.

I get the following error:

cannot convert from 'System.Threading.Tasks.ValueTask<int>' to 'int'

All of the examples I have found for assigning [Parameter] values to Razor Components are synchronous, and the only asynchronous values I have found are for callbacks and methods (not parameters).

Further, searching online did not return anything obvious so I am posting here in hopes of finding an answer.

Note that I am aware of using protected override async Task OnInitializedAsync and storing a value in there, but that seems like a lot of required ceremony compared to the approach above, especially when considering the multiple services and properties that I will ultimately have to bind.

So, how does one assign values from an asynchronous call to a Razor Component [Parameter] property in the way that I would prefer?

4

4 Answers

1
votes

The problem is, Counter.Get() isn't an int value; it's a Task that will have an int at some undefined point either now or in the future. So you can't assign its value to something that's expecting an int right now, because that int doesn't necessarily exist yet.

You've already got the answer, and though it "seems like a lot of ceremony", it's really the only way to do this:

  • Create an int property to hold the value.
  • Declare an async method
  • In that method, assign the awaited value of Counter.Get() to the int that's holding the value
  • Set the component's Count property equal to the int property

It may feel like a lot of ceremony, but you should be grateful. Asynchrony is inherently very complicated, and having async/await available already takes care of about 95% of the hard work for you. If you think this solution is messy, you oughtta see what it would take to get it right without async/await!

1
votes

Try this.

@page "/"
@inject ICountingService Counter

<h1>Hello World!</h1>

<MyComponent Count="@CounterValue" />

@code{
    
     public int CounterValue {get; set;}
     protected override async Task OnInitializedAsync()
     {
           CounterValue = await Counter.Get();
     }
}
1
votes

After @mason-wheeler and @rich-bryant provided their answers, I went to think about this a little more and found my solution, which I have posted here:

https://github.com/Mike-E-angelo/Blazor.ViewProperties

I am calling it a ViewProperty which looks like the following:

    public interface IViewProperty
    {
        ValueTask Get();
    }

    public sealed class ViewProperty<T> : IViewProperty
    {
        public static implicit operator ViewProperty<T>(ValueTask<T> instance) => new ViewProperty<T>(instance);

        readonly ValueTask<T> _source;

        public ViewProperty(ValueTask<T> source) => _source = source;

        public T Value { get; private set; }

        public bool HasValue { get; private set; }

        public async ValueTask Get()
        {
            Value    = await _source;
            HasValue = true;
        }

        public override string ToString() => Value.ToString();
    }

You then pair it with a component base type that then iterates through the component's view properties and invokes their respective asynchronous operations:

    public abstract class ViewPropertyComponentBase : ComponentBase
    {
        protected override async Task OnParametersSetAsync()
        {
            var properties = GetType().GetRuntimeProperties();
            foreach (var metadata in properties.Where(x => x.GetCustomAttributes<ParameterAttribute>().Any() &&
                                                           typeof(IViewProperty).IsAssignableFrom(x.PropertyType)))
            {
                if (metadata.GetValue(this) is IViewProperty property)
                {
                    await property.Get().ConfigureAwait(false);
                }
            }
        }
    }

A sample razor component that uses the above:

MyComponent.razor

@inherits ViewPropertyComponentBase

@if (Count.HasValue)
{
    <p>Your magic number is @Count.</p>
}
else
{
    <p>Loading, please wait...</p>
}


@code {

    [Parameter]
    public ViewProperty<int> Count { get; set; }

}

The resulting use is a clean view with direct bindings and no need for overrides or other additional ceremony:

@page "/"
@inject ICounter Count

<h1>Hello, world!</h1>

Welcome to your new app.

<MyComponent Count="@Count.Count()" />

(NOTE that my posted example and above uses reflection, which is slow. In the actual version of the solution that I am using, I compile the member access as lambda expressions and cache the result. You can find that by starting here if you are brave enough to poke around.)

0
votes

It feels a bit hacky, but you could do something like this:

<MyComponent Count="@Counter.Get().Result" />