Brian
Brian

Reputation: 1989

How can I invoke StateHasChanged() or update my UI from an implemented interface within a .net Blazor .RAZOR page?

I have a Blazor Server application. With this application, I am "talking" with MistralAI. This REST API endpoint has a streaming token you can set that will allow it to "stream" responses to you.

In my application, I have a service that contacts MistralAI for this conversation. I have implemented the Observer pattern so that I can update my .RAZOR page whenever I get a response from MistralAI:

MyService.cs:

public class MyService {
    private List<Reader> readers = new List<Reader>();
    private AIResponse response;
    public async Task<AIResponse> myTask(){
        Reader newReader = new MyPage();
        readers.Add(newReader);
        // Send requests to and receive responses from MistralAI
        addWord(receivedJSONString);
        notifyReaders();
    }

    public void notifyReaders()
    {
        foreach (var reader in readers) {
            reader.updateWords(response);
        }
    }

    public void addWord(AIResponse newResponse)
    {
        response = newResponse;
    }
}

public interface Reader {
    void updateResponses(AIResponse newResponse);
}

MyPage.razor

@implements Reader
<div>
    @foreach(var newResponse in responses){
        @newResponse
    }
</div>
@code {
    private List<AIResponse> responses { get; set; } = new List<AIResponse>();
    private async Task QueryAI(){
        AIResponse streamingAIResponse = await myService.myTask(someString);
    }

    public void updateResponses(AIResponse newResponse){
        responses.Add(newResponse);
        Console.Write(newResponse);
        StateHasChanged();
    }
}

I can successfully get the responses to my .RAZOR page, I can see them in the console when I Console.Write, but I cannot update the UI with the new content; I keep getting the error The render handle is not yet assigned.

So, how can I use the StateHasChanged() from within my implemented interface?

Or, how can I update my UI with the new content?


UPDATES:

@code {
    [Inject]
    public IMyService myService { get; set; }

    private String queryString { get; set; } = null;

    private List<AIResponse> responses { get; set; } = new List<AIResponse>();

    private Conversation conversation { get; set; }

    private ElementReference aiQuery;

    protected override void OnInitialized()
    {
        if (conversation == null)
        {
            conversation = new onversation();
            conversation.messages = new List<Message>();
        }
    }

    private async Task QueryAI()
    {
        Message message = new Message("user", queryString);
        conversation.messages.Add(message);


        StreamingQueryResponse streamingAIResponse = await myService.getStreamingAIResponseAsync(conversation);
    }

    public void updateWords(StreamingQueryResponse newResponse)
    {
        responses.Add(newResponse);
        Console.Write(newResponse);
    }
}

Upvotes: 0

Views: 97

Answers (2)

Bennyboy1973
Bennyboy1973

Reputation: 4246

The observer pattern generally has a service with an event you subscribe to in your consuming page.

The following isn't working code, but should have all the elements to understand the service / page relationship:

-the service does all the API interactions. It sets up the async call, and every time new message info comes in, it fires the event

-the page or component subscribes to the service event by adding an event handler to process each message, and (importantly) removes it in Dispose.

MistralService.cs

namespace MyApp.Code
{
    public class MistralService
    {
            public event Action<string> OnMessageReceived;

            public async Task StartStreamingAsync(string apiKey)
            {
                using var httpClient = new HttpClient();
                var request = new HttpRequestMessage(HttpMethod.Get, "mistral.api/endpoint");
                request.Headers.Add("Authorization", $"Bearer {apiKey}");

                using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

                using var stream = await response.Content.ReadAsStreamAsync();
                using var reader = new StreamReader(stream);

                while (!reader.EndOfStream)
                {
                    var line = await reader.ReadLineAsync();
                    OnMessageReceived?.Invoke(line);
                }
        }
    }
}

ConsumingPage.razor

@using MyApp.Code
@inject MistralService MS

<h3>ConsumingPage</h3>
<button @onclick=StartStreaming>Start Streaming</button>
<div>@streamedText</div>
@code {
    string streamedText = "";
    protected override void OnInitialized()
    {
        MS.OnMessageReceived += HandleMessage;
    }
    async Task StartStreaming()
    {
        await MS.StartStreamingAsync("theAPIkey");
    }
    async void HandleMessage(string Message)
    {
        streamedText += Message + "<br/>";
        await InvokeAsync(StateHasChanged);
    }
    public void Dispose()
    {
        MS.OnMessageReceived -= HandleMessage;
    }
}

Remember to add MistralService to your service in Program.cs:

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
builder.Services.AddScoped<MistralService>();

Upvotes: 1

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30310

You can't do this - manually create a component. That's the job of the Renderer.

Reader newReader = new MyPage();

You need to decouple the service that receives the messages and the UI component that displays them.

To do this your service exposes a NewMessage event that anyone who want to receive a notification subscribes to - in this case your display components.

This is the Blazor Notification Pattern. You can see an implementation example here that you can adapt to fit your needs.

Upvotes: 0

Related Questions