I'm trying to make a chat between two clients that you can choose from the connected clients list. So far everything is working good - I have the list and I succeeded to send messages to all clients by using Clients.All.SendAsync.
The problem starts when I'm trying to use
Clients.Client(connectionId).
In my client side (Angular), where I'm displaying the connected clients list, I've added a button to each client so you can send him a message:
I have click event to send the connectionId to the server, so it will know who to send a message to. I debugged the program and saw that I'm reaching to the server with the right data (the connectionId to send him a message) and when I'm getting back to the client no message is printed. Also I'm not getting any error on the console. What am I doing wrong?
Server side:
Controller-
[HttpPost("SendMessageToUser")]
public Task<IActionResult> SendMessageToUser([FromBody] ChatMessageDto chatMessageDto)
{
IActionResult actionResult;
if (chatMessageDto.Validate())
{
Response response=_signalrService.SendMessageToUser(chatMessageDto.ConnectionId,chatMessageDto.Message);
actionResult=Ok(response);
}
else
{
actionResult = BadRequest();
}
return Task.FromResult(actionResult);
}
Service-
public Response SendMessageToUser(string connectionId, string message)
{
Response response;
UserHub hub = new UserHub(_userHubContext);
hub.SendMessageToUser(message, connectionId);
response = new Response
{
Message = message,
IsSuccess = true
};
return response;
}
Hub-
public void SendMessageToUser(string message, string connectionId)
{
_hubContext.Clients.Client(connectionId).SendAsync("GetMessage", message).Wait();
}
Client side:
user.ts component - only partial code that relevant (there is continuance in my computer)
startSignalrConnection(): void {
this.signalRService
.startSignalrConnection(this.signalrConnectionUrl)
.then((signalrHubConnectionId) => {
firstValueFrom(
this.http.post(
this.addClientUrl,
//get user name from local storage
buildNewChatClientConnectionModel(
this.chatClientId,
signalrHubConnectionId,
this.userName!
)
)
)
.then((response) => {
this.signalRService.addListenersMsg();
// Add function call for getting all connected users
this.userService.getConnectedUsers().subscribe((users) => {
this.users = users;
});
user.html component-
<div *ngFor="let user of users">
<div class="users-list">
<div><strong>Id: </strong>{{user?.chatClientId}}</div>
<div><strong>ConnectionId: </strong>{{user?.connectionId}}</div>
<div><strong>Name: </strong>{{user?.name}}</div>
<button class="button" [disabled]="chatMessage.length==0" (click)="sendMessageToUser(user?.connectionId)">Send Message</button>
<hr>
</div>
</div>
sendMessageToUser (on the same component)-
sendMessageToUser(sendToConnId: any): void {
this.sendToConnId=sendToConnId;
this.signalRService.sendMessageToUser(
this.sendMessageToUserUrl,
this.chatMessage,
this.sendToConnId,
);
}
SignalR service-
public addListenersMsg() {
this.hubConnection.on("GetMessage", (message: string, connectionId: string) => {
this.messages.push(buildDisplayChatMessageModel(new Date(), message));
});
}
public sendMessageToUser(sendMessageToUserUrl: string, message: string, sendToConnId: any){
firstValueFrom(this.http.post(sendMessageToUserUrl, buildChatMessageModel(sendToConnId, message)))
.then((response) => {
console.log(response);
})
.catch((error) => {
console.log("Failed to send message: ", error);
alert("Failed to send message: "+ error);
})
}
Related
I'm attempting to recreate in Blazor WASM a login scenario originally developed in an Angular SPA wherein I use an HttpIntercepter to catch any 401 responses, pop open a login window which redirects to our ADFS login, then closes and returns the login information and retries the failed (401) request. Here's what it looks like in Angular:
Angular LoginInterceptor
export class LoginInterceptor implements HttpInterceptor {
constructor(private loginService: LoginService) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
catchError((errorResponse: HttpErrorResponse) => {
switch (errorResponse.status) {
case 401:
{
console.log("Unauthorized");
// call the LoginService's openLoginWindow and wait for it to complete
return this.loginService.openLoginWindow().pipe(
mergeMap((result) => {
if (result) {
// retry the request again
return next.handle(req);
}
})
);
}
default:
break;
}
throw errorResponse;
})
) as Observable<HttpEvent<any>>;
}
}
Angular LoginService
export class LoginService {
loginWindow: Window;
userName: BehaviorSubject<string> = new BehaviorSubject(null);
private windowsMessageObservable: Observable<MessageEvent>;
constructor() {
// Handle the Window.OnMessage event which listens for a successful login message in the new window
this.windowsMessageObservable = fromEvent<MessageEvent>(window, 'message');
}
openLoginWindow() {
// Open the new window
this.loginWindow = window.open("/SSOSignIn", 'loginWindow');
// Return an observable that fires when the login message is received
const signInObservable = new Observable<boolean>(obs => {
this.windowsMessageObservable.subscribe(evt => {
if (evt.origin === location.origin) {
if (evt.data?.type === 'signIn') {
this.userName.next(evt.data.name);
obs.next(true)
}
}
});
});
return signInObservable;
}
}
This works great in Angular. When a page loads or if a login expires, a request for data fails with 401, gets intercepted, pops up the login window which closes automatically after SSO completes, and the request seamlessly retries without having to reload or reclick a button, but in Blazor/C#, I can't seem to wrap my head around how I can retry the original request since we're not dealing with observables.
In Blazor/C#, as far as I can tell, the concept of HttpInterceptors are implemented using DelegatingHandlers. I've created a handler which pops open the login window and signs in, but I don't know a good way to hold off retrying and returning the response until after the login completes. Here's my handler:
namespace BlazorPlayground.Client.Handlers
{
public class UnauthorizedMessageHandler : DelegatingHandler, IDisposable
{
public UnauthorizedMessageHandler(IJSRuntime iJSRuntime)
{
JS = iJSRuntime;
}
private IJSRuntime JS { get; set; }
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
// This opens a new window but the operation continues immediately after.
// Need to somehow wait for login to complete here so I can retry request
await JS.InvokeVoidAsync("openUrl", "/SSOSignIn", "_blank");
}
return response;
}
}
}
Any ideas? Basically I need this SendAsync code to wait for a JS Window.Message event before completing and returning a response.
Ok I figured out a working solution. The basic concept I came up with was: create an await-able task that completes when the function is invoked from JS. The key behind this is to use the TaskCompletionSource<> which allows you to wait for it to be marked complete from anywhere. In my case I'm waiting for the TaskCompletionSource<> to complete when my Window.Message event handler invokes my C# ReceiveMessage method. Here's my interceptor:
public class UnauthorizedMessageHandler : DelegatingHandler, IDisposable
{
private DotNetObjectReference<UnauthorizedMessageHandler> objRef;
private TaskCompletionSource<string> tcs;
public UnauthorizedMessageHandler(IJSRuntime iJSRuntime)
{
JS = iJSRuntime;
}
private IJSRuntime JS { get; set; }
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
// Create a reference to this object so ReceiveMessage can be invoked from JS
objRef = DotNetObjectReference.Create(this);
// This allows us to wait for another method to complete before we continue
tcs = new TaskCompletionSource<string>();
// Open up the sign-in page
await JS.InvokeVoidAsync("openUrl", "/SSOSignIn", "_blank", objRef);
// Wait until ReceiveMessage is fired off
var message = await tcs.Task;
// Retry the original request
response = await base.SendAsync(request, cancellationToken);
}
return response;
}
[JSInvokable]
public void ReceiveMessage(string message)
{
// Get the message from JS and return it to the awaitable task
tcs.TrySetResult(message);
}
}
Here's my javascript
var windowMessageObjRef = null;
window.addEventListener('message', (evt) => {
// Make sure the message came from us
// Need to add checks to make sure it's got the data we expect
if (evt.origin === location.origin) {
// Check to make sure we have a reference to our DotNet interop object
if (windowMessageObjRef) {
// Send the name of the person who logged in
console.log('Invoking ReceiveMessage with data ' + evt.data.name);
windowMessageObjRef.invokeMethodAsync('ReceiveMessage', evt.data.name);
}
}
});
function openUrl(url, target, objRef) {
if (objRef) {
windowMessageObjRef = objRef;
}
console.log("Opening " + url + " with target " + target);
window.open(url, target);
}
Since this is a SPA application I don't want to leave the original page so my SSOSignIn page is popped opened in a new tab/window which just fires off the login challenge which redirects to ADFS and returns us to the SSOComplete page:
public class SSOSignInModel : PageModel
{
public ChallengeResult OnGet()
{
return Challenge(new AuthenticationProperties
{
RedirectUri = "/SSOComplete"
});
}
}
And the SSOComplete page posts the message to the opener (SPA) window with the name of the signed-in user and then closes itself.
<html>
<head>
<title>Redirecting to sign-in...</title>
</head>
<body>
<script type="text/javascript">
(function () {
var message = {
type: 'signIn',
success: true,
name: '#User.Identity.Name'
};
window.opener.postMessage(message, location.origin);
window.close();
})();
</script>
</body>
</html>
Now I have the ability in Blazor to automatically pop up the signin window and retry the original request after the sign-in completes in Blazor without having to reload my SPA. I'm gonna go take a nap now.
I am developing a .Net core project with Angular on Frontend and c# on backend. I am new to web development and i am looking for ideas or little help on achieving a task.
I am connecting my Angular frontend to an External server using .net web server. I am able to post data to External server successfully using Http services. After i receive response from external server the results are queued in my controller. Before i send response to client i want to enable a download button on Client, only if i receive response from the External server and the results are queued in my controller successfully. If there is no response from External server I do not want to enable download button.
Appcomponent.html
<button mat-button id="getdoc" (click) = "getdoc()" [disabled] = "disabled" >Download</button>
Appcomponent.ts
getdoc() {
this.download.downloadDoc()
.subscribe(res => {
this.datadownload = new Blob([res], { type: 'application/txt' });
// { this.disabled = false };
saveAs(this.datadownload);
console.log(this.datadownload);
}, error => {
console.log('error while downloading', error)
})
}
Appservice.ts
export class DownloadService {
constructor(private http: HttpClient) { }
downloadDoc(): Observable<any> {
return this.http.get('URL', {/* headers, */ reportProgress: true, responseType : "blob"});
}
}
Controller
namespace ang.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class DoController : Controller
{
private void OnProvideData(DataAndHeader dat)
{
CommHeader header = (CommHeader)dat.Header;
switch (header.Type)
{
case CommHelpers.CommType.ServerToClientReceived:
break;
case CommHelpers.CommType.ServerToClientFinished:
string ClientID = header.ClientID;
ConcurrentQueue<DataAndHeader> queueResultsForClient = null;
if (!dicResults.TryGetValue(ClientID, out queueResultsForClient))
{
queueResultsForClient = new ConcurrentQueue<DataAndHeader>();
if (!dicResults.TryAdd(ClientID, queueResultsForClient))
{
}
}
queueResultsForClient.Enqueue(dat);
break;
}
}
[HttpGet]
// [Route("download")]
public byte[] download(string ClientID)
{
// some logic
return new byte[0];
}
}
}
I want to wait till i get the Response from the External Server and loaded into my Controller after that i want to enable the button. After the button is enabled and clicked by Client he should be able to download file.
The "getdoc()" works only when i click download button. but initially the download button is in disable state. I want to programatically enable it from my controller not from my Client
Thanks in Advance.
Try the following code:
getdoc() {
this.download.downloadDoc()
.subscribe(res => {
this.datadownload = new Blob([res], { type: 'application/txt' });
saveAs(this.datadownload);
console.log(this.datadownload);
this.disabled = false; // This is the change
}, error => {
this.disabled = true;
console.log('error while downloading', error)
})
}
So I have a C# WebApi Signalr webservice with a Angular 2 Signalr Client.
The WebService is supposed to update the client when a new message comes in from a post method that other services may call.
[HttpPost]
public void NotificationMessage([FromBody]ServiceInformation serviceInformation)
{
Messages.notificationMessage = serviceInformation;
notificationMessage.BroadCastNotificationMessage();
}
Another service posts to this method and this method sets a static variable in the Messages class and then calls the NotificationMessage Hub.
public class NotificationMessage : Hub<IClient>
{
public void BroadCastNotificationMessage()
{
Clients.All.serviceInfoBroadCast(JsonConvert.SerializeObject(Messages.notificationMessage));
}
}
The notification hub then calls Clients.All to broadcast the new message to all clients.
Angular Site:
Service:
constructor() {
this.connectionEstablished = new EventEmitter<Boolean>();
this.messageReceived = new EventEmitter<ServiceInformationObject>();
this.connectionExists = false;
this.connection = $.hubConnection(CONFIGURATION.baseUrls.server);
this.proxy = this.connection.createHubProxy(this.proxyName);
this.registerOnServerEvents();
this.startConnection();
}
private startConnection(): void {
this.connection.start({ jsonp: true, waitForPageLoad: false}).done((data: any) => {
console.log('Now connected ' + data.transport.name + ', connection ID= ' + data.id);
this.connectionEstablished.emit(true);
this.connectionExists = true;
//this.proxy.invoke('BroadCastNotificationMessage');
}).fail((error: any) => {
console.log('Could not connect ' + error);
this.connectionEstablished.emit(false);
});
}
private registerOnServerEvents(): void {
this.proxy.on('serviceInfoBroadCast', (data: string) => {
console.log('received in SignalRService: ' + JSON.stringify(data));
let jsonData = JSON.parse(data);
let newData = new ServiceInformationObject(jsonData.ServiceName, jsonData.Message, jsonData.State, jsonData.MachineName, Date.now());
this.messageReceived.emit(newData);
});
}
I setup the hub connection followed by the hub proxy. I call the proxy.on method connecting to the Clients.All dynamic method from the web service. Then I start the connection.
Component to display data on view:
private subscribeToEvents(): void {
this._signalRService.connectionEstablished.subscribe(() => {
this.canSendMessage = true;
});
this._signalRService.messageReceived.subscribe((message: ServiceInformationObject) => {
this._ngZone.run(() => {
this.testArray.push(message);
});
});
}
The issue:
If I leave the invoke call in the startConnection method, it will pull data down from the webservice, but it will never update ever again.
If I don't use the invoke nothing happens.
I'm not sure why the webservice is not pushing the information when Clients.All is called. I know the post method is being called from my own interal logging and I know the object is not empty.
Any ideas on why the webservice is not pushing the information? Or why the client is not displaying it?
I have a MVC 5 backend written in C#. It serves MVC views written in Razor and also some Angular 2 pages.
What is the best way to handle potential errors when calling server from client? I really would like to establish a pattern that is robust and works in all situations. Below is what I have tried so far.
Backend C# code:
public class MyController : Controller
{
[HttpGet]
public ActionResult GetUsers()
{
try
{
// Lot of fancy server code ...
throw new Exception("Dummy error");
return GetCompressedResult(json);
}
catch (Exception ex)
{
throw new HttpException(501, ex.Message);
}
}
private FileContentResult GetCompressedResult(string json)
{
// Transform to byte array
var bytes = Encoding.UTF8.GetBytes(json);
// Compress array
var compressedBytes = bytes.Compress();
HttpContext.Response.AppendHeader("Content-encoding", "gzip");
return new FileContentResult(compressedBytes, "application/json");
}
}
Client side Angular 2 code:
public loadDataFromServer() {
let response = this.http.get(this.urlGetData)
.map((res: Response) => res.json())
.catch(this.handleError);
response.subscribe(response => {
// Process valid result ...
},
err => { console.error(err); }
);
};
private handleError(error: Response | any) {
let errMsg: string;
if (error instanceof Response) {
const body = JSON.parse(JSON.stringify(error || null))
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
This is a printscreen of the error object processed by handleError method:
This all raises some questions:
Is it correct to throw custom HttpException from server?
Is handleError method correct or maybe too complex?
On client side I would like to see the custom error message, but currently it is just found in an enormous "blob" of HTML that is nasty to parse.
Is client side error handling necessary BOTH in get call and subscribe action?
My current suggestion is to let server respond with Json object for all handled exceptions.
On client side I check result object for possible error property before handling valid result.
The handleResponseError method will parse typed Response object and throw observable message. But at least my browser (Chrome 57) seems to automatically log response errors to console. So if subscriber need no specific extra handling for different errors, then the subscriber need no extra action for err object.
Please feedback if there are better ways!
Backend C# code:
public class MyController : Controller
{
[HttpGet]
public ActionResult GetUsers()
{
try
{
// Lot of fancy server code ...
throw new ArgumentException("Dummy error");
// Normal return of result ...
}
catch (Exception ex)
{
return Json(new { error = $"{ex.GetType().FullName}: '{ex.Message}'" }, JsonRequestBehavior.AllowGet);
}
}
}
Client side Angular 2 code:
public loadDataFromServer() {
let response = this.http.get(this.urlGetData)
.map((res: Response) => res.json())
.catch(this.handleResponseError);
response.subscribe(result => {
if (result.error) {
this.displayJsonError(this.urlGetUsers, result.error);
}
else {
// Process valid result
}
});
};
private handleResponseError(value: Response | any) {
let errorMessage = value.toString();
let response = value as Response;
if (response) {
errorMessage = `${response.status}: ${response.statusText}\n${response.toString()}`;
}
if (value.error) {
errorMessage = value.error;
}
if (value.message) {
errorMessage = value.message;
}
return Observable.throw(errorMessage);
}
private displayJsonError(url: string, error: string) {
console.error(`Call to '${url}' failed with ${error}`);
}
I have implemented SignalR for my Windows Azure project. I have two clients - Javascript/HTML client in my web role and a console application in my project. And Web role is my SignalR server. When i put the web role and the console application as the start up projects, the messages i send from the HTML client are sent to the console application. But when i put the Cloud project and the console application as the start up projects, the messages from the HTML client are not being sent to the console application. Its really weird, i dont know what could be the difference between the two which is causing the problem.
And if i put a background thread in my web role which will send messages to connected clients periodically, it works on both occasions, i mean the console app and the HTML client are receiving messages irrespective of the start up projects.
Please let me know if you have any idea what the problem is
My Hub:
public class BroadcastHub : Hub
{
public void Send(PersistedAudioRecord record)
{
// Call the BroadcastAudio method to update clients.
Clients.All.BroadcastAudio(record);
}
}
My HTML/Javascript client:
<script type="text/javascript">
$(function () {
// Declare a proxy to reference the hub.
var broadcast = $.connection.broadcastHub;
// Create a function that the hub can call to broadcast messages.
broadcast.client.broadcastAudio = function (record) {
// Html encode user name, channel and title.
var encodedName = $('<div />').text(record.Username).html();
var encodedChannel = $('<div />').text(record.Channel).html();
var encodedTitle = $('<div />').text(record.Title).html();
// Add the broadcast to the page.
$('#broadcasts').append('<li><strong>' + encodedName
+ '</strong>: ' + encodedChannel + '</strong>: ' + encodedTitle + '</li>');
};
// Get the user name.
$('#displayname').val(prompt('Enter your name:', ''));
// Get the Channel name to which you want to broadcast.
$('#channelname').val(prompt('Enter Channel:', ''));
// Set initial focus to message input box.
$('#title').focus();
// Start the connection.
$.connection.hub.start().done(function () {
$('#sendbroadcast').click(function () {
// Call the Send method on the hub.
var broadcastMessage = {}
broadcastMessage.Username = $('#displayname').val();
broadcastMessage.Channel = $('#channelname').val();
broadcastMessage.Title = $('#title').val();
broadcast.server.send(broadcastMessage);
// Clear text box and reset focus for next broadcast.
$('#title').val('').focus();
});
});
});
</script>
My Console app client:
class Program
{
static void Main(string[] args)
{
HubConnection connection = new HubConnection("http://localhost:35540/");
IHubProxy proxy = connection.CreateHubProxy("BroadcastHub");
proxy.On<AudioRecord>("BroadcastAudio", BroadcastAudio);
connection.Start().Wait();
Console.ReadLine();
}
static void BroadcastAudio(AudioRecord record)
{
Console.WriteLine("Broadcast: {0} {1} {2}", record.Username, record.Channel, record.Title);
}
}
Background Thread:
public class BackgroundThread
{
private static Random _random = new Random();
public static void Start()
{
ThreadPool.QueueUserWorkItem(_ =>
{
IHubContext hubContext = GlobalHost.ConnectionManager.GetHubContext<BroadcastHub>();
while (true)
{
PersistedAudioRecord record = new PersistedAudioRecord();
record.Channel = _random.Next(10).ToString();
record.Username = new string('a', Convert.ToInt32(record.Channel));
record.Title = new string('b', Convert.ToInt32(record.Channel));
try
{
hubContext.Clients.All.BroadcastAudio(record);
}
catch (Exception ex)
{
System.Diagnostics.Trace.TraceError("SignalR error thrown: {0}", ex);
}
Thread.Sleep(TimeSpan.FromSeconds(2));
}
});
}
}
I tried this scenario with my application and I was able to send messages from a webrole to a console application. Is it possible to zip your project and send it to see if this reproes...