May 6, 2021
This tutorial will show how you can use Telerik's Blazor Server-Side UI Components with Weavy acting as backend to build a chat app in a .NET Blazor Server Project.
https://showcase.weavycloud.com
Start by cloning the project from Github. It contains all the code you need for this tutorial.
https://showcase.weavycloud.com
and is fully functional. BUT, if you are interested in setting up your own local Weavy instance as the acting backend, you can also clone the Weavy solution used in this tutorial from Github.The app will showcase functionality for a chat app built with Telerik Blazor UI Components. It is not a production ready application and should be regarded as an example on how to leverage Weavy functionality in a Telerik/Blazor project.
These are the functions we are going to use in this tutorial:
By default, authentication is hardcoded against the public Weavy instance. There are four different user accounts you can sign in with. Authentication is done using static JWT tokens with long expiry solely for demo purposes. Every time a request is made for the Weavy API that static JWT token will be passed along as a Bearer token.
Authorization: Bearer <jwt-token>
The token will be un-wrapped and validated in Weavy. If validation succeeds (it will ;-), you will be authenticated as the user you signed in with.
Clicking a user will sign you in as that user, using cookie-based authentication. After signing in you will be presented with a menu for app navigation.
Navigate to /chat
to view the application. There is a lot going on here, we'll break it down for you.
The Chat Application.
/Pages/Chat.razor
.The conversations are fetched using the Weavy REST API when the components are first initialized. Calls to the API is routed through a simple C# class called ChatService
. It contains functions that map to the ones in the Weavy API and simply performs the HTTP calls and deserializes the JSON
responses.
// invoked when the component is initialized after having received its initial parameters
protected override async Task OnInitializedAsync() {
...
conversations = await ChatService.GetConversationsAsync();
...
}
When the conversations property has been set, Telerik will automatically bind and populate the conversations since the conversations
property is defined as the Data attribute of a ListView component. You can read more about Telerik Blazor data binding in this Telerik Getting Started article.
<TelerikListView Data="@conversations" Class="conversations">
<Template>
<div class="listview-item conversation" @onclick="@(_ => LoadConversation(context.id))">
<img src="@context.thumb.AsAbsoluteUrl()" alt="">
<h3>@context.title</h3>
<div class="p">@context.last_message?.html</div>
</div>
</Template>
</TelerikListView>
The template defines how to render the items. Notice the onclick
handler. When triggered a call will be made to the LoadConversation
function with the id of the conversation to load:
// loads messages from a conversation and updates the UI
protected async Task LoadConversation(int conversationId) {
isMessagesLoading = true;
activeConversation = conversationId;
// make sure the correct css class is used in the listview
conversations = conversations.Select(x => { x.selected = x.id == activeConversation ? true : false; return x; }).ToArray();
// get messages in conversation
messages = await ChatService.GetMessagesAsync(conversationId);
if (messages != null && messages.data != null) {
// some grouping of messages to get the markup we need
messageMarkup = new MarkupString(GetMarkup(messages.data.ToArray()));
} else {
messageMarkup = new MarkupString("");
}
// clear loading indicator
isMessagesLoading = false;
}
Again, the Weavy API is responsible for returning the actual data we need. In this case the messages of the conversation that was clicked. The message objects returned are then turned into a string with the markup we need in order to render the content of the conversation. When the messageMarkup
property is updated it will automatically re-render the messages view.
Did you notice the statement: isMessagesLoading = true;
? That boolean property is data bound to a <TelerikLoader />
component. When the property is true
the component is visible:
<TelerikLoader Class="loader-indicator" ThemeColor="primary" Visible="@isMessagesLoading" Type="LoaderType.InfiniteSpinner" Size="LoaderSize.Large"></TelerikLoader>
A super easy way to let the user know the UI is being updated.
No conversations to display? Then we need to add some.
Click the New Conversation... button and select the members you want to be included in the conversation. The TelerikMultiSelect
component is fed it's result from all available chat users in Weavy. After you click create the API is called and the conversation is created. The UI is then updated to reflect the new data.
<TelerikWindow Size="WindowSize.Small" Centered="true" @bind-Visible="addConversationModalVisible" Width="400px" Modal="true">
<WindowTitle>
<strong>Add Conversation</strong>
</WindowTitle>
<WindowActions>
<WindowAction Name="Close" />
</WindowActions>
<WindowContent>
<TelerikMultiSelect Data="@users.data" TextField="title" TItem="User" TValue="int" ValueField="id" @bind-Value="@selectedMembers" AutoClose="false" Placeholder="Select Conversation Members" Width="100%"></TelerikMultiSelect>
<TelerikButton OnClick="@(_ => CreateConversation())" Primary="true" ButtonType="ButtonType.Button" Class="create">Create</TelerikButton>
</WindowContent>
</TelerikWindow>
Alot of databinding going on here: The TelerikMultiSelect
is getting its data from the Weavy API via the Data attribute and the members that the user selects are bound to the selectedMembers
property. All controls are wrapped in a TelerikWindow
component for a nice presentaion.
Realtime events in Weavy are pushed to users using SignalR. In order to recieve realtime events from the Weavy instance, we first hook us up against the Weavy realtime-hub
using the javascript client.
var wvy = wvy || {};
wvy.interop = (function ($) {
var weavyConnection = $.hubConnection("https://showcase.weavycloud.com/signalr", { useDefaultPath: false });
// enable additional logging
weavyConnection.logging = true;
// log errors
weavyConnection.error(function (error) {
console.warn('SignalR error: ' + error)
});
var rtmProxy = weavyConnection.createHubProxy('rtm');
rtmProxy.on('eventReceived', function (name, data) {
// log incoming event
console.debug(name, data);
// when we receive an event on the websocket we publish it to our subscribers via js interop
DotNet.invokeMethodAsync('WeavyTelerikBlazor', 'ReceivedEvent', name, data);
});
// connect to weavy realtime hub
function connect() {
weavyConnection.start().done(function () {
console.debug("weavy connection:", weavyConnection.id);
}).fail(function () {
console.error("could not connect to weavy");
});
}
return {
connect: connect
}
})(jQuery);
wwwroot\js\interop.js
All events are recieved here and then routed to C# (on the server) using a Blazor techology called JavaScript interoperability (JS interop), which enables javascript to call .NET methods and vice versa.
After some additional framework magic the event ends up in the Chat.razor
component and can easily be handled. You can find a list of available events in the Server API / Weavy.Core.Events section on docs.weavy.com.
...
// called after a component has finished rendering
protected override async Task OnAfterRenderAsync(bool firstRender) {
if (firstRender && user.Identity.IsAuthenticated) {
// connect to weavy for listening to realtime events
await JS.InvokeVoidAsync("wvy.interop.connect");
// add event handler for realtime event
Realtime.OnMessage += HandleMessage;
Realtime.OnTyping += HandleTyping;
}
}
...
// event handler for new message events
async void HandleMessage(object sender, MessageEventArgs e) {
Console.WriteLine($"Chat received message event");
if (e.Data.conversation == activeConversation) {
await LoadConversation(e.Data.conversation);
} else {
conversations = await ChatService.GetConversationsAsync();
}
StateHasChanged();
}
// event handler for typing events
void HandleTyping(object sender, TypingEventArgs e) {
Console.WriteLine("@" + e.Data.user.username + " is typing in conversation " + e.Data.conversation);
}
So far we have only been using Weavy as an API in order to get the chat app up and running. Now we are going to utilize the Weavy Drop-in UI. You'll see how easy it is to incorporate the Weavy built in UI in an app.
The Weavy Client SDK has been packaged as Blazor component and using it is super simple.
<Weavy>
<WeavyApp SpaceKey="telerik-blazor-ui" Height="100vh" key="posts" type="posts" />
</Weavy>
/Pages/Index.razor
Here you can see the Drop-in UI in action. If the user is authenticated, the <Weavy>
component will load.
For a visible UI of a perticular app type you simply add the app you want as a <WeavyApp>
element. In this case a space with a key of telerik-blazor-ui
will be created or fetched and a post app will be added to the space and then rendered in place. Here's the result:
It's really easy to get started using the Weavy Drop-in UI. With just a few lines of code you get feature complete apps that can be created on the fly.
Weavy is highly extendable, enabling developers the ability to modify most parts of the product. For this project we have been using REST API endpoints that we created custom for this project. Being able to modify and extend the Weavy REST API can be a powerful tool for building applications that are efficient and complex. Below you can see the endpoints used in this tutorial.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;
using System.Web.Http.Description;
using Weavy.Areas.Api.Models;
using Weavy.Core;
using Weavy.Core.Models;
using Weavy.Core.Services;
using Weavy.Web.Api.Controllers;
using Weavy.Web.Api.Models;
namespace Weavy.Areas.Api.Controllers {
/// <summary>
/// Api controller for manipulating Conversations.
/// </summary>
[RoutePrefix("api")]
public class ConversationsController : WeavyApiController {
/// <summary>
/// Get the <see cref="Conversation" /> with the specified id.
/// </summary>
/// <param name="id">The conversation id.</param>
/// <example>GET /api/conversations/527</example>
/// <returns>The specified conversation.</returns>
[HttpGet]
[ResponseType(typeof(Conversation))]
[Route("conversations/{id:int}")]
public IHttpActionResult Get(int id) {
// read conversation
ConversationService.SetRead(id, DateTime.UtcNow);
var conversation = ConversationService.Get(id);
if (conversation == null) {
ThrowResponseException(HttpStatusCode.NotFound, $"Conversation with id {id} not found.");
}
return Ok(conversation);
}
/// <summary>
/// Get all <see cref="Conversation" /> for the current user.
/// </summary>
/// <example>GET /api/conversations</example>
/// <returns>The users conversations.</returns>
[HttpGet]
[ResponseType(typeof(IEnumerable<Conversation>))]
[Route("conversations")]
public IHttpActionResult List() {
var conversations = ConversationService.Search(new ConversationQuery());
return Ok(conversations);
}
/// <summary>
/// Create a new or get the existing conversation between the current- and specified user.
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost]
[ResponseType(typeof(Conversation))]
[Route("conversations")]
public IHttpActionResult Create(CreateConversationIn model) {
string name = null;
if (model.Members.Count() > 1) {
name = string.Join(", ", model.Members.Select(u => UserService.Get(u).GetTitle()));
}
// create new room or one-on-one conversation or get the existing one
return Ok(ConversationService.Insert(new Conversation() { Name = name }, model.Members));
}
/// <summary>
/// Get the messages in the specified conversation.
/// </summary>
/// <param name="id">The conversation id.</param>
/// <param name="opts">Query options for paging, sorting etc.</param>
/// <returns>Returns a conversation.</returns>
[HttpGet]
[ResponseType(typeof(ScrollableList<Message>))]
[Route("conversations/{id:int}/messages")]
public IHttpActionResult GetMessages(int id, QueryOptions opts) {
var conversation = ConversationService.Get(id);
if (conversation == null) {
ThrowResponseException(HttpStatusCode.NotFound, "Conversation with id " + id + " not found");
}
var messages = ConversationService.GetMessages(id, opts);
messages.Reverse();
return Ok(new ScrollableList<Message>(messages, Request.RequestUri));
}
/// <summary>
/// Creates a new message in the specified conversation.
/// </summary>
/// <param name="id"></param>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost]
[ResponseType(typeof(Message))]
[Route("conversations/{id:int}/messages")]
public IHttpActionResult InsertMessage(int id, InsertMessageIn model) {
var conversation = ConversationService.Get(id);
if (conversation == null) {
ThrowResponseException(HttpStatusCode.NotFound, "Conversation with id " + id + " not found");
}
return Ok(MessageService.Insert(new Message { Text = model.Text, }, conversation));
}
/// <summary>
/// Called by current user to indicate that they are typing in a conversation.
/// </summary>
/// <param name="id">Id of conversation.</param>
/// <returns></returns>
[HttpPost]
[Route("conversations/{id:int}/typing")]
public IHttpActionResult StartTyping(int id) {
var conversation = ConversationService.Get(id);
// push typing event to other conversation members
PushService.PushToUsers(PushService.EVENT_TYPING, new { Conversation = id, User = WeavyContext.Current.User, Name = WeavyContext.Current.User.Profile.Name ?? WeavyContext.Current.User.Username }, conversation.MemberIds.Where(x => x != WeavyContext.Current.User.Id));
return Ok(conversation);
}
/// <summary>
/// Called by current user to indicate that they are no longer typing.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpDelete]
[Route("conversations/{id:int}/typing")]
public IHttpActionResult StopTyping(int id) {
var conversation = ConversationService.Get(id);
// push typing event to other conversation members
PushService.PushToUsers("typing-stopped.weavy", new { Conversation = id, User = WeavyContext.Current.User, Name = WeavyContext.Current.User.Profile.Name ?? WeavyContext.Current.User.Username }, conversation.MemberIds.Where(x => x != WeavyContext.Current.User.Id));
return Ok(conversation);
}
/// <summary>
/// Marks a conversation as read for the current user.
/// </summary>
/// <param name="id">Id of the conversation to mark as read.</param>
/// <returns>The read conversation.</returns>
[HttpPost]
[Route("conversations/{id:int}/read")]
public Conversation Read(int id) {
ConversationService.SetRead(id, readAt: DateTime.UtcNow);
return ConversationService.Get(id);
}
/// <summary>
/// Get the number of unread conversations.
/// </summary>
/// <returns></returns>
[HttpGet]
[ResponseType(typeof(int))]
[Route("conversations/unread")]
public IHttpActionResult GetUnread() {
return Ok(ConversationService.GetUnread().Count());
}
}
}
These events can easily be hooked up to using the Weavy Client SDK.
This tutorial has been trying to demonstrate how you can use the Telerik Blazor UI framework to work with data that is being served from and persisted in Weavy. Weavy is designed around well known standards and provides great flexibility when it comes to building your own stuff.
Select what technology you want to ask about
Get answers faster with our tailored generative AI. Learn more about how it works
This is a custom LLM for answering your questions. Answers are based on the contents of the documentation and this feature is experimental.
To access live chat with our developer success team you need a Weavy account.