Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

ChatView | Scenarios

Every ChatView scenario is the same two moving parts: an IChatSessionProvider and a SessionId. What changes is the permissions you return from Info and a couple of control flags. The snippets below assume a provider like the one in The Provider Interface.

Show history with no composer. Hide the input bar and return None permissions.

<shiny:ChatView Provider="{Binding Provider}"
SessionId="{Binding SessionId}"
IsInputBarVisible="False" />
public ChatSessionInfo BuildInfo() => new(
SessionId, SessionName, Users,
PermittedEmojis: [], // no reactions
BodyPermissions: MessageBodyPermissions.None,
Permissions: ChatSessionPermissions.None,
CreatedAt, LastReadDate, UnreadMessageCount: 0
);

Paging, live MessageReceived, and image taps still work — only composing is removed.

A 1:1 conversation where the customer can send text and images and react, but not edit/delete or manage the session.

public ChatSessionInfo BuildInfo() => new(
SessionId, "Support", Users,
PermittedEmojis: ["👍", "❤️", "🎉"],
BodyPermissions: MessageBodyPermissions.Bold | MessageBodyPermissions.Italics | MessageBodyPermissions.Links,
Permissions: ChatSessionPermissions.CanSendMessages
| ChatSessionPermissions.CanSendImages
| ChatSessionPermissions.CanReactToMessages,
CreatedAt, LastReadDate, UnreadMessageCount
);

Drive delivery/read status from your backend by sending Sent/Delivered/Read statuses and raising MessageUpdated(MessageChangeKind.StatusChanged). Show the agent’s typing indicator via UserTyping, and surface connectivity with ConnectionStateChanged (see Typing & Connection).

Treat the assistant as another participant. The provider sends the user’s message, then streams or posts the assistant’s reply via MessageReceived (optionally after a UserTyping(true) “thinking” burst, like the demo’s SimulateReplyAsync).

public Task<ChatMessage> SendMessageAsync(OutgoingMessage message, CancellationToken ct = default)
{
var stored = /* persist the user's message with the same ClientMessageId */;
_ = this.RunAssistantAsync(message.Body); // raise UserTyping then MessageReceived
return Task.FromResult(stored);
}

For action cards (approve/decline, suggestions), stamp Identifier/Metadata and render with a message template.

Grant the management permissions; the control surfaces invite/leave/rename affordances automatically.

public ChatSessionInfo BuildInfo() => new(
SessionId, "Controls Crew", Users, // Users[] includes avatars + per-user BubbleColor
PermittedEmojis: null, // default emoji set
BodyPermissions: MessageBodyPermissions.All,
Permissions: ChatSessionPermissions.All,
CreatedAt, LastReadDate, UnreadMessageCount
);

Implement the management methods and announce membership/name changes:

public Task InviteUserAsync(string userId, CancellationToken ct = default)
{
var joined = this.AddUser(userId);
this.UserJoined?.Invoke(this, joined);
this.SessionUpdated?.Invoke(this, this.Info); // refresh Info first, then announce
return Task.CompletedTask;
}
public Task RenameAsync(string sessionName, CancellationToken ct = default)
{
this.store.Rename(sessionName);
this.SessionUpdated?.Invoke(this, this.Info);
return Task.CompletedTask;
}
public Task LeaveAsync(CancellationToken ct = default) => this.RemoveSelfAsync();

Per-user read receipts and per-participant bubble colors come from ReadReceipts and each ChatSessionUserInfo.BubbleColor.

Bind SessionId to the selected conversation; the control disposes the old session and resolves the new one on change. One provider can host many sessions.

@inject IChatSessionProvider ChatProvider
<select @onchange="OnPick">
@foreach (var c in contacts) { <option value="@c.Id">@c.Name</option> }
</select>
<div style="height: 600px;">
<ChatView Provider="ChatProvider" SessionId="@sessionId" />
</div>
@code {
string sessionId = "alice";
void OnPick(ChangeEventArgs e) => sessionId = e.Value?.ToString() ?? sessionId;
}

When ChatView lives inside a FloatingPanel, set IsContentScrollEnabled="False" on the panel and give the ChatView no HeightRequest. The chat scrolls itself, so nesting it in the panel’s ScrollView collapses it, and a fixed height either overflows the panel — hiding the composer — or forces FitContent="True", which collapses the panel to a single detent you can no longer drag.

Leave AdjustForKeyboard at its default. ExpandOnInputFocus raises the panel when the composer is focused, but once the panel is at its top detent that padding is the only thing lifting the composer clear of the keyboard.

<shiny:FloatingPanel Detents="{Binding SheetDetents}"
IsContentScrollEnabled="False"
ExpandOnInputFocus="True">
<shiny:ChatView Provider="{Binding Provider}"
SessionId="{Binding SessionId}" />
</shiny:FloatingPanel>
public ObservableCollection<DetentValue> SheetDetents { get; } = [DetentValue.Half, DetentValue.Full];

On Blazor, wrap the chat in an element with height: 100% inside <SheetContent> — the sheet sizes its body to the visible detent band, so the chat fills it and the input bar stays on screen.

<SheetView @bind-IsOpen="isOpen" Detents="detents">
<SheetContent>
<div style="height:100%">
<ChatView Provider="ChatProvider" SessionId="@sessionId" />
</div>
</SheetContent>
</SheetView>

If GetSessionAsync (or the initial load) throws ChatSessionException — unknown session or no access — the control renders an error state instead of the chat. Throw it from the provider:

public Task<IChatSession> GetSessionAsync(string sessionId, CancellationToken ct = default)
{
if (!this.stores.TryGetValue(sessionId, out var store))
throw new ChatSessionException($"Chat session '{sessionId}' was not found.");
return Task.FromResult<IChatSession>(new InMemoryChatSession(store));
}