Search code examples
blazorblazor-server-sideobserver-pattern

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

Solution

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