17
votes

I simply cannot figure out how to make a signalr connection from Angular.

Using the following tutorial at https://docs.microsoft.com/en-us/aspnet/signalr/overview/getting-started/tutorial-getting-started-with-signalr-and-mvc

I've added a new SignalR 2.4.0 project to an existing .Net 4.6 solution in vs2017.

I also have an Angular 7 application to which I've added the SignalR package via npm install @aspnet/signalr

Now I'm trying to hook up a simple connection between client and server, but can't figure out how to establish the initial connection.

My front end keeps throwing an exception:

 core.js:15714 ERROR Error: Uncaught (in promise): Error: Cannot send data if the connection is not in the 'Connected' State.

Error: Cannot send data if the connection is not in the 'Connected' State.

In my front end search component, I've added some fields for testing:

<mat-form-field>
    <input matInput placeholder="message" [(ngModel)]="message">
</mat-form-field>
<button mat-button type="button" (click)="sendMessageToServer()"><span>Send</span></button>            
<p *ngFor="let m of messages">{{m}}</p>

And in my ts file :

// import other components/services here..
import { HubConnection, HubConnectionBuilder} from '@aspnet/signalr';

@Component({
  selector: 'app-my-search',
  templateUrl: './my-search.component.html',
  styleUrls: ['./my-search.component.scss']
})
export class MySearchComponent implements OnInit {

public hubConnection: HubConnection;
  public messages: string[] = [];
  public message: string;

   constructor() { }
   
   
  ngOnInit() {
   
    // SIGNALR MESSAGE HUB
    let builder = new HubConnectionBuilder();
    this.hubConnection = builder.withUrl('/SynBroadcastHub/BroadcastMessage').build();  // see startup.cs
    this.hubConnection.on('notifyUser', (message) => {
      this.messages.push(message);
      console.log(message);
    });
    this.hubConnection.start();
  }

  // signalr, send msg from client
  sendMessageToServer() {
    this.hubConnection.invoke('MessageToServer', this.message);
    this.message = '';
  }


}

and on the c# side, I added a SignalR Hub Class (v2) file, BroadcastHub.cs

using Microsoft.AspNet.SignalR;

namespace SynBroadcastHub
{
    public class BroadcastHub : Hub
    {        
        /// Message to client 
        public void BroadcastMessage(string data)
        {
            Clients.Caller.notifyUser(data);
        }
    
        
        /// Message from client application; broadcast to all clients if requested.                
        public void MessageToServer(string data, bool notifyAllClients = false)
        {
            if (notifyAllClients)
            {
                Clients.All.NotifyAllClients(data);
            }
        }
    }
}

as well as a Startup.cs file :

using Microsoft.Owin;
using Microsoft.AspNet.SignalR;
using Owin;

[assembly: OwinStartup(typeof(SynBroadcastHub.Startup))]

namespace SynBroadcastHub
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            HubConfiguration cfg = new HubConfiguration();
            app.MapSignalR<PersistentConnection>("BroadcastHub/BroadcastMessage");
            app.MapSignalR(cfg);
            app.MapSignalR();

            //app.MapSignalR<NotifyHub>("notify"); ???
        }
         public override Task OnDisconnected(bool stopCalled)
        {
            return Clients.All.leave(Context.ConnectionId, System.DateTime.Now.ToString());
        }

        public override Task OnConnected()
        {
            return Clients.All.joined(Context.ConnectionId, DateTime.Now.ToString());
        }

        public override Task OnReconnected()
        {
            return Clients.All.rejoined(Context.ConnectionId, DateTime.Now.ToString());
        }
    }
}
4
My first problem: The @aspnet/signalr is the WRONG package for my 4.6 framework project. The doc says JavaScript and TypeScript clients for SignalR for ASP.NET Core. The correct package for asp.net/framework is signalr.bob.mazzo
@NarottamGoyal - I'm trying to revisit this. We're using 4.6 framework, not core. But it seems that using core with Angular 8 at this point is the best option moving forward.bob.mazzo

4 Answers

34
votes

I just spent two days trying to figure out this same thing. I finally got it to work and these are the few things that i had to do:

1) You noted that using the @aspnet/signalr package was incorrect for .Net framework, and that is correct. You need the signalr package (npm install signalr).

2) This is the most critical part of the whole process. SignalR has a dependency on jQuery. You have to include jQuery before including the signalr script. In the angular.json file, under the scripts section, you need to include:

"./node_modules/jquery/dist/jquery.js", "./node_modules/signalr/jquery.signalR.js"

in that exact order. On start up of your project, it will load jQuery first, then the signalR script.

Many other stackover flow answers answering the question in reply to this error:

jQuery was not found. Please ensure jQuery is referenced before the SignalR client JavaScript file

tell you to write import * as $ from "jquery" in the component you are wanting to use jQuery in. However, it is NOT correct to do this. The reason is, according to this angular article about global scripts, using the import syntax will include it in the module load and put it in the vendor.js file that is created from running an ng build command. The reason this is a problem is because jQuery will get loaded first from your angular.json, then signalR will be loaded, then the module from the vendor.js will RELOAD jQuery and unattach all the events that were just attached to jQuery from signalR.

3) Since you noticed you were using the .Net Core version of signalr, you wont have access to the HubConnectionBuilder when trying to instantiate a new HubConnection in your angular component.

Instead, when the signalr script gets executed, it will attach additional events to the $ in your code. Note: if you get errors on build or in compile time from your ts files, make sure you've included the @types/signalr and @types/jquery from npm

To set up a new hub connection, use $.hubConnection("your-url-here/signalr"). This will attach to your server's Hub when running. Note: I stored the result from this as a variable called hubConnection in my angular component

On your server code (.cs file), where you have your Hub class, you will need to add above the class name: [HubName("YourHubName")]. So in your case your .cs file would look something like this at the top:

[HubName("Broadcast")]    
public class BroadcastHub : Hub

You will most likely have to include this at the top of your .cs file: using Microsoft.AspNet.SignalR.Hubs;

Then in your Angular Component you set up a proxy to attach to that Hub on your server The very next line after instantiating your new hubConnection, write:

this.hubConnection.createHubProxy("yourHubName");.

In your case, this.hubConnection.createHubProxy("broadcast");

After you have made your proxy, you can then attach listeners to listen to events emitted from the server, or you can invoke server functions from your angular components.

I followed this example here to learn how to set up calling events and listening to server events. Yes it is angular 2, but the functions from signalr all still work the same in my angular 7 app.

Short answer: use proxy.on('eventname') to listen to events from the server, and use proxy.invoke('eventname') to call functions on your Hub from your angular components.

Lastly, a few notes in your cs files. In my Startup.cs, the only thing i have for mapping signalr is app.MapSignalR(). I did not go into many details regarding other properties to set like you have done, but that may be another cause of some issues?

2
votes
  • Angular app

Install signalR package

npm i @aspnet/signalr --save

import { Component, OnInit } from '@angular/core';
import { HubConnection } from '@aspnet/signalr';
import * as signalR from '@aspnet/signalr';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  private hubConnection: HubConnection;

  public ngOnInit() {
    this.hubConnection = new signalR.HubConnectionBuilder()
      .withUrl("http://localhost:50930/pushNotification").build();

    this.hubConnection.start().then(() => {
      console.log("connection started");
    }).catch(err => console.log(err));

    this.hubConnection.onclose(() => {
      debugger;
      setTimeout(() => {
        debugger;
        this.hubConnection.start().then(() => {
          debugger;
          console.log("connection started");
        }).catch(err => console.log(err));
      }, 5000);
    });

    this.hubConnection.on("clientMethodName", (data) => {
      debugger;
      console.log(data);
    });

    this.hubConnection.on("WelcomeMethodName", (data) => {
      debugger;
      console.log(data);
      this.hubConnection.invoke("GetDataFromClient", "user id", data).catch(err => console.log(err));
    });
  }

  public stopConnection() {
    this.hubConnection.start().then(() => {
      console.log("stopped");
    }).catch(err => console.log(err));
  }
}
  • Web API with netcoreapp2.2

    Install Microsoft.AspNetCore.SignalR

Startup.cs

Client is running on port 4200 ("http://localhost:4200").

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace SignalRWebApp
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            services.AddCors(option =>
            {
                option.AddPolicy("CorsPolicy", builder =>
                         builder.WithOrigins("http://localhost:4200")
                        .AllowAnyMethod()
                        .AllowAnyHeader()
                        .AllowCredentials());
            });
            services.AddSignalR();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseCors("CorsPolicy");

            app.UseSignalR(routes =>
            {
                routes.MapHub<SignalHub>("/pushNotification");
            });

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}

SignalHub.cs

using Microsoft.AspNetCore.SignalR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SignalRWebApp
{
    public class SignalHub : Hub
    {
        public void GetDataFromClient(string userId, string connectionId)
        {
            Clients.Client(connectionId).SendAsync("clientMethodName", $"Updated userid {userId}");
        }

        public override Task OnConnectedAsync()
        {
            var connectionId = Context.ConnectionId;
            Clients.Client(connectionId).SendAsync("WelcomeMethodName", connectionId);
            return base.OnConnectedAsync();
        }

        public override Task OnDisconnectedAsync(Exception exception)
        {
            var connectionId = Context.ConnectionId;
            return base.OnDisconnectedAsync(exception);
        }
    }
}

Now send signalR message like the below example

[Route("api/[controller]")]
    [ApiController]
    [EnableCors("CorsPolicy")]
    public class ValuesController : ControllerBase
    {
        private IHubContext<SignalHub> _hub;
        public ValuesController(IHubContext<SignalHub> hub)
        {
            _hub = hub;
        }

        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            _hub.Clients.All.SendAsync("clientMethodName", "get all called");
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{connectionId}")]
        public ActionResult<string> Get(string connectionId)
        {
            _hub.Clients.Client(connectionId).SendAsync("clientMethodName", "get called");
            return "value";
        }
    }
}

Github

1
votes

Was just researching this subject myself, and found npm package ng2-signal. Maybe something to look into yourself?

1
votes

Are you sure that the hubEndpoint is correct? It seems like the hub is part of the angular routing (judging by the way you are writing it). Try to set the full path (ex. https://www.myapp.com/SynBroadcastHub/BroadcastMessage)