shuhelohelo’s blog

C#、WPF、Xamarin.Formsが好きですが、ASP.NET Coreも好きです。

GitHubにPushしたときにrejectedされてしまったときの対処法

PowerShellのプロファイルをGitHubで管理しようとして,GitHubリポジトリを作成し,ローカルのPowerShellのフォルダをPushしようとしたらrejectされた.

> git push origin master
To https://github.com/hoge/fuga.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'https://github.com/hoge/fuga.git'

解決には以下の記事が参考になった.

qiita.com

GitHubリポジトリを作成した際にReadMeを作成するオプションを選択していたため,空のリポジトリではない状態になっていた.

そこで,ローカル側のリポジトリをPushしようとして,「関係ないリポジトリ同士をマージするな」ということでエラーが出ていたようだ.

確かにそのとおりだな,と.

上の記事によると,そのような操作はデフォルトで禁止されているということで,それを「わかった上でやりますよ」というオプションが--allow-unrelated-histories.

助かった.

オプションをつけてmergeを実行する.

> git merge --allow-unrelated-histories origin/master
Merge made by the 'recursive' strategy.
...(処理継続)

PowerShellでGitを使いやすくする

git-scm.com

Windowsでgitやdotnetawsなどのコマンドラインツールを使いたい。

でもコマンドプロンプトは使いたくない、lsと打って怒られるのはもう嫌だ、というときPowershellだと若干そのあたりの不満を解消できます。

しかしながらgitをインストールしてもデフォルトでは今いるブランチがどこなのか,ひと目でわからないため使いにくかったりします.

そこで,posh-gitです.PowerShell Git.

「.NET Conf 2019」でもプレゼンターが使っていました.

posh-gitはPowershell用のgitクライアントです.

これを導入することで,

導入前: f:id:shuhelohelo:20191022020103p:plain

導入後: f:id:shuhelohelo:20191022020231p:plain

このように今いるブランチがウィンドウのタイトルとプロンプトに表示され,Gitのステータスも表示されるため,現在のブランチの変更状況を把握しやすくなります.

プロンプトに表示される情報の意味は,posh-gitのGitHubリポジトリのReadMeに詳しく書いてあります.

github.com

インストール手順

Git for Windowsはインストール済みとします.

インストール方法は上記のReadMeにも書いてあるとおりいくつかあるのですが,PowerShell Gallaryからコマンド一つでインストールするのが楽です.

Install-Module posh-git -Scope CurrentUser -AllowPrerelease -Force

また,パッケージ管理ツールのchocolateyからもインストールできるので,chocolateyを使っている方にはこちらも便利です.

Posh-gitの有効化

以下のコマンドで有効化します.

Import-Module posh-git

有効化してからPowerShellを再起動してGit管理されたフォルダに移動すると,前述のようにGitの情報が表示されます.

PowerShell起動時にPosh-gitを有効化するには,PowerShellのプロファイルに以下の情報を書きます.

Import-Module posh-git

プロファイルはデフォルトでは以下の場所にあります.

Windows PowerShellの場合:

C:\Users\{UserName}\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1

PowerShell 7の場合:

C:\Users\{UserName}\Documents\PowerShell\profile.ps1

プロンプトのカスタマイズ

プロファイルにいろいろ書くことで,プロンプトの表示を変更できます.

例えばこちらとか.

qiita.com

こちらの設定を使用すると,以下のようになります. プロンプトが2行目に表示されるようになりました. 入力位置,ブランチの情報が左端にまとまって見やすくなりました.

f:id:shuhelohelo:20191022023113p:plain

その他にはこちらとか.

opcdiary.net

こちらはプロンプトに表示されるパスが長い場合に短縮してくれます.末尾から16文字という設定とのことです.

そこで,この2つを組み合わせて,

  • Gitの情報とプロンプトを左側に集める
  • パスが長い場合は短縮する

という設定にしてみました.

Import-Module posh-git

function global:prompt {
    $realLASTEXITCODE = $LASTEXITCODE

    # # Reset color, which can be messed up by Enable-GitColors
    # $Host.UI.RawUI.ForegroundColor = $GitPromptSettings.DefaultForegroundColor

    $pro = ''

    Write-VcsStatus

    if ($pwd.ProviderPath.ToString().Length -gt 19) {
        if ((Split-Path($pwd.ProviderPath) -Leaf).ToString().Length -gt 16) {  #C:\分は削除して
            $pro = $pwd.Drive.Name + ':\..' + (Split-Path($pwd.ProviderPath) -Leaf)
        }
        else {
            $start = $pwd.ProviderPath.ToString().Length - 16
            $pro = $pwd.Drive.Name + ':\..' + $pwd.ProviderPath.Substring($start)
        }
        
    }
    else {
        $pro = $pwd.ProviderPath
    }

    Write-Host($pro) -nonewline -ForegroundColor Green

    Write-Host 


    $global:LASTEXITCODE = $realLASTEXITCODE
    return "$('>' * ($nestedPromptLevel + 1)) "
}

$global:GitPromptSettings.BeforeText = '['
$global:GitPromptSettings.AfterText  = '] '

これによって,以下のようにスッキリ見やすくなりました.

f:id:shuhelohelo:20191022025502p:plain

だいぶPowerShellでGitを使いやすくなりました.

20191022追記:

プロンプトのパスの表示をカレントディレクトリ名だけにしました.

こっちのほうがよりスッキリするかなと思って.フルパスが知りたいときはpwdコマンドを使うことにします.

こちらのサイトを参考にしました.

qiita.com

このように表示されます.

f:id:shuhelohelo:20191022113552p:plain

プロファイルの内容は以下のとおりです.

Import-Module posh-git

function global:prompt {
    $realLASTEXITCODE = $LASTEXITCODE

    # # Reset color, which can be messed up by Enable-GitColors
    # $Host.UI.RawUI.ForegroundColor = $GitPromptSettings.DefaultForegroundColor

    # posh-gitの出力
    Write-VcsStatus

    # カレントディレクトリの出力.改行無し
    $idx = $pwd.ProviderPath.LastIndexOf("\")+1
    Write-Host($pwd.ProviderPath.Remove(0, $idx)) -nonewline

    # 改行
    Write-Host 

    $global:LASTEXITCODE = $realLASTEXITCODE
    return "$('>' * ($nestedPromptLevel + 1)) "
}

# Gitの情報を表示する部分を括る文字を変更する.
$global:GitPromptSettings.BeforeText = '['
$global:GitPromptSettings.AfterText  = '] '
# $global:GitPromptSettings.DefaultPromptAbbreviateHomeDirectory=$true

posh-gitの表示情報の見方

posh-gitが有効になるとGit管理下のディレクトリでは以下のようにGitの情報が表示されます.

[master +0 ~0 -1 ~] EggOrHen

この表示内容は以下を表しています.

  • +{n} : 追加されたファイルの数
  • ~{n} : 変更されたファイルの数
  • -{n} : 削除されたファイルの数
  • ~ : ステージング済みだがコミットされていないファイルがある
  • ! : ステージングされていないファイルがある

上記の例でいえば,削除されたファイルが1つあり,コミットされていないファイルがあることがわかります.

[master +0 ~0 -1 ~] EggOrHen
> git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        deleted:    EggOrHen/MyClass.cs

他の例を見てみます.

[develop ≡ +0 ~5 -0 !] TakeMeThereXamarinForms
  • : ローカルとリモートで同じコミットを指している
  • ↑{num} : ローカルがリモートよりコミットが進んでいる.pushできるよ.
  • ↓{num} : リモートがローカルよりもコミットが進んでいる.pullできるよ.
  • {num a}↕{num b} : ローカルとリモートをあわせるにはpushとpullが必要な状況.rebaseしたほうがいい?

直感的でわかりやすいですね.

上の例では,5つの変更されたファイルがあり,ステージングされていないファイルがある,ということですね.

[develop ≡ +0 ~5 -0 !] TakeMeThereXamarinForms
> git status
On branch develop
Your branch is up to date with 'origin/develop'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   TakeMeThereXamarinForms/TakeMeThereXamarinForms/App.xaml
        modified:   TakeMeThereXamarinForms/TakeMeThereXamarinForms/App.xaml.cs
        modified:   TakeMeThereXamarinForms/TakeMeThereXamarinForms/Models/TargetInformation.cs
        modified:   TakeMeThereXamarinForms/TakeMeThereXamarinForms/Utility.cs
        modified:   TakeMeThereXamarinForms/TakeMeThereXamarinForms/Views/MainPage.xaml

ではこれをすべてステージングすると表示はどのように変わるでしょうか.

[develop ≡ +0 ~5 -0 !] TakeMeThereXamarinForms
> git add .
[develop ≡ +0 ~5 -0 ~] TakeMeThereXamarinForms
>

!~に変わりました.

ではファイルを1つだけステージングに追加してみます.

[develop ≡ +0 ~5 -0 !] TakeMeThereXamarinForms
> git add TakeMeThereXamarinForms/TakeMeThereXamarinForms/App.xaml
[develop ≡ +0 ~1 -0 | +0 ~4 -0 !] TakeMeThereXamarinForms
>

すると,このように|で区切られて,左側がステージングの情報,右がアンステージングの情報というように表示されます.

SignalRの自動再接続をもう少しくわしく.

環境

自動再接続を有効にする

SignalRの自動再接続を有効にするには,HubConnectionBuilderのインスタンス生成でwithAutomaticReconnect()を追加するだけ.

Javascriptの例:

www.jerriepelser.com

            var connection = new signalR.HubConnectionBuilder()
                .withUrl('/chathub') //startupのMapHubで指定したHubのURLを指定する.
                .withAutomaticReconnect()//自動再接続
                .build();

再接続の試行は所定の時間間隔で行われ,指定もできるがデフォルトでは切断されてから2秒,10秒,30秒経過のタイミングで再接続が行われる.

このタイミングは明示的に指定することもできて,その場合は以下のように配列を使ってミリ秒で指定する.

withAutomaticReconnect([2000, 100000, 300000])//デフォルトと一緒の設定

C#の例:

            connection = new HubConnectionBuilder()
                .WithUrl("https://localhost:44350/chathub")//アプリケーションからの接続の場合はフルで書く
                .WithAutomaticReconnect()
                .Build();

これも同じくデフォルトでは2秒,10秒,30秒経過のタイミングで再接続.

間隔を指定するには以下のようにTimeSpanの配列をWithAutomaticReconnectメソッドにわたす.

            connection = new HubConnectionBuilder()
                .WithUrl("https://localhost:44350/chathub")//アプリケーションからの接続の場合はフルで書く
                .WithAutomaticReconnect(new[] { TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30) })
                .Build();

再接続のカスタマイズ

指定した配列の分だけ再接続が行われると,以後は再接続が行われない.

それでよい場合はあるが,回数の制限なく再接続を試みたいときもある.

その場合は以下のように設定する.

Javascriptの場合:

            const connection = new signalR.HubConnectionBuilder()
                .withUrl('/chathub')
                .withAutomaticReconnect({
                    nextRetryDelayInMilliseconds: retryContext => {
                        return Math.random() * 10000; //0~10秒のランダムな間隔で再接続を試行.
                    }
                })
                .build();

C#の場合:

            IRetryPolicy retryPolicy = new RandomRetryPolicy();

            connection = new HubConnectionBuilder()
                .WithUrl("https://localhost:44350/chathub")
                .WithAutomaticReconnect(retryPolicy)
                .Build();
    public class RandomRetryPolicy : IRetryPolicy
    {
        private readonly Random _random = new Random();

        public TimeSpan? NextRetryDelay(RetryContext retryContext)
        {
            //2~5秒の間でランダムに再接続を試みる
            return TimeSpan.FromSeconds(_random.Next(2,5));
        }
    }

再接続の間隔はランダムに設定するとよい. これは複数のクライアントから同じタイミングでリクエストが送られることによるサーバー側の負荷を軽減するため.

SignalRでプロパティを持つオブジェクトを渡す. WPF ⇔ Server

環境

前回までは単純なstring型をサーバーとクライアント間でやりとりしていました.

shuhelohelo.hatenablog.com

当然そのような基本型だけでなく,以下のようなプロパティを持つオブジェクトのやりとりもできます.

    public class Message
    {
        public string SenderName { get; set; }
        public string Body { get; set; }
        public DateTimeOffset SendDateTime { get; set; }
    }

送り主の名前,本文,送信時刻というプロパティを持つシンプルなオブジェクトです.

クライアント側もサーバー側も,送受信に関してはこれまでと変わりません.

ただ,今まで引数の型や型引数でstringを指定していたところにMessage型を指定すればよいだけです.

具体的には以下のようになります.

サーバー側:

        public async Task SendObject(Message message)
        {
            await Clients.All.SendAsync("ReceiveObject", message);
        }

クライアント側:

            //受信部分
            _connection.On<Message>("ReceiveObject", (message) => {
                this.Dispatcher.Invoke(()=> {
                    messagesList.Items.Add($"{message.Body}_{message.SenderName}_{message.SendDateTime.ToString()}");
                });
            });
            //送信部分
            //送信するオブジェクトを作成
            var message = new Message {
                SenderName = "WPF",
                Body = messageTextBox.Text,
                SendDateTime = DateTimeOffset.Now,
            };

            //送信
            try
            {
                await _connection.InvokeAsync("SendObject",message);
            }
            catch(Exception ex)
            {
                this.Dispatcher.Invoke(()=> {
                    messagesList.Items.Add(ex.Message);
                });
            }

さて,このMessageクラスですが,サーバーとクライアントの双方が同じものを使用する必要があるので,クラスライブラリとして作成し,それをサーバー,クライアントの双方で参照に追加しています.

今回のようにクライアントとサーバーがどちらもC#で実装されていると,こういったところがとても便利ですね.

しかしながら,Webアプリケーションだとクライアント側はJavascriptをはじめとしたC#以外の言語が使用されることがほとんどです.(でもBlazorならC#C#!)

次はJavascript ⇔ Serverでオブジェクトを渡してみようと思います.

今回の分.

github.com

SignalRの自動再接続

環境

前回はサーバーとSignalRで通信を行うクライアントをWPFで作成しました.

shuhelohelo.hatenablog.com

リアルタイム通信ですが途中で接続が切れてしまった場合に自動的に再接続することで,利便性が高まります.

SignalRは自動再接続の仕組みがあり,とても簡単な記述でこの機能を実現することができます.

基本的にはHubConnectionBuilderでWithAutomaticReconnectを入れるだけです.

            connection = new HubConnectionBuilder()
                .WithUrl(@"https://localhost:44350/chathub")//サーバー側でMapHubで指定したURLを指定する.
                .WithAutomaticReconnect()
                .Build();

これだけで自動再接続を行ってくれるようになります.

わかりやすいように接続状態の変化ごとにメッセージが表示されるようになっています.

サーバーとクライアントを起動させて,クライアントからサーバーに接続してから,サーバーを落とします.

すると再接続を試み,Reconnectingと表示されます.でも,サーバーが起動していないので接続はできません.

次にサーバーを起動すると,Reconnectedと表示され,メッセージを送れることが確認できます.

f:id:shuhelohelo:20191008165523p:plain

WPFからSignalRでサーバーとリアルタイム通信

前回はサーバとブラウザ間でSignalRを用いた通信を試してみました.

shuhelohelo.hatenablog.com

今回は,デスクトップアプリケーションからSignalRを使ってサーバーと通信してみます.

チュートリアルは以下の公式サイトが参考になるでしょう.

docs.microsoft.com

環境

端的に言って,書き方などはブラウザでやったのとほとんど同じです.

まずはNugetでMicrosoft.AspNetCore.SignalR.Clientをインストールします.

次にHubConnectionのインスタンスを作成します.

        public MainWindow()
        {
            InitializeComponent();

            //接続のためのHubConnectionのインスタンスを作成する.
            connection = new HubConnectionBuilder()
                .WithUrl(@"https://localhost:44350/chathub")//サーバー側でMapHubで指定したURLを指定する.
                .Build();
        }

次にサーバー側から呼び出される処理をHubConnection.Onメソッドを使って登録しておく.

受け取るデータの種類,個数を型引数で指定し,サーバーから呼び出すための名前を指定し,メッセージを受け取ったときの処理を登録します.

下の例はサーバー側からReceive(大文字小文字は関係ない)という名前で呼び出すことができ,引数は文字列(string)一つ.

            //サーバー側から呼び出される処理を定義
            //サーバー側からReceiveを指定して呼び出しがあったときに,
            //登録したDelegateが実行される.
            connection.On<string>("Receive", (message) =>
            {
                //Dispatcher使っている理由は?
                //UIに直接書き込むのであればUIスレッドで行う必要があるため
                this.Dispatcher.Invoke(() =>
                {
                    var newMessage = $"{message}";
                    messagesList.Items.Add(newMessage);
                });
            });

(注意) connection.Onメソッドでの処理の登録はイベントハンドラと同じで累積していきます. 同じ記述が実行されるたびに2つ,3つと累積していくので,同じメッセージが1回で2つ,3つドバっと送られることになります. なのでちゃんと管理しておく必要があります.

特定の登録を削除したい場合はconnection.Remove("Receive")のように処理につけた名前を指定することで一括削除されます

と,これだけ用意すれば,クライアントサーバー間でリアルアイム通信する準備は整いました.

あとは,以下のメソッドでサーバーとSignalRで接続されます.

                await connection.StartAsync();

それでは実行してみましょう.

それぞれのプロジェクトをCtrl+F5で実行します.

ブラウザ側はhttps://localhost:<<port番号>>/index.htmlを開きます.

WPF側は最初に接続ボタンを押して,サーバーと接続します.Connection startedと表示されたら,テキストボックスに文字を入力して送信ボタンを押せば,メッセージが送信されます.

f:id:shuhelohelo:20191008095400p:plain

このようにブラウザ,WPF両方のクライアントにお互いのメッセージが表示されることが確認できます.

おわりに

リアルタイム双方向通信がこんなに簡単にできてしまうなんて素晴らしいですね.

しかも,クライアント側の処理の書き方がC#Javascriptで同じように書けるというのも,開発コストが低くて良いです.

クライアント側の基本的な書き方が,

  1. HubのURLを指定
  2. connection.Onで処理を記述
  3. connection.StartAsyncで開始

ととてもシンプルです.

今後の発展としては以下が考えられます.

  • 接続が切れたときに自動再接続されるようにする
  • stringなどの基本型ではなく任意のオブジェクトを受け渡しする
  • サーバーからのメッセージをReactive Extensionで扱う
  • Azure SignalRを使う

blog.xin9le.net

今回のソースコードGitHubで公開しています. (2019.10.08:修正しました)

github.com

.NET Conf 2019のSignalRのデモを動かす

www.youtube.com

こちらの動画を見ながら,1つ目のデモと同じリアルタイムチャットアプリを作る.

ソースコードは公開されていないが,画面を凝視すればコードが読めるし,説明は丁寧でわかりやすい.

環境

プロジェクトの作成

ASP.NET Core WebApplicationのプロジェクトを,Emptyなテンプレートを使ってをdotnet core 3.0で作成する.

Starup.csでSignalRを使用する設定を行う

ある機能をASP.NET Coreで有効にする場合は,おなじみのStartup.ConfigureServicesメソッドで使うサービスを登録し,Startup.Configureメソッドで使用することを明記する.

SignalRの場合,以下のようにservices.AddSignalR()をConfigureServicesに加え,Configureメソッド内のUserEndpoints内でendpoints.MapHub<Chat>("/chat");とルーティングを設定する.

...
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services.AddSignalR();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();//クライアントサイドのライブラリにアクセスするために必要.
            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapHub<ChatHub>("/chathub");
            });
        }

サーバーサイドの実装

上でChatHubクラスを実装していないのでそれを作る.

ここでMapHubクラスにわたすクラスはHubクラスを継承したもので,文字通り通信のハブとなる役割を担うものである.

以下のようなHubを作成する.

using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRCorePractice
{
    internal class ChatHub:Hub
    {
        public Task Send(string message)
        {
            return Clients.All.SendAsync("Receive",message); //クライアント側のreceiveメソッドを呼ぶ.
            //クライアント側のconnection.Onメソッドで指定しているもの
        }
    }
}

このChatHubクラスにSendという名前のメソッド定義することにする. これはクライアントがメッセージを送る際に呼び出すメソッド. このメソッドは,Clients.All.SendAsync("Send",message);とあるように,全てのクライアントに対して送られてきたデータ(message)を全てのクライアントに対して送信する.

ネットワーク越しにメソッドを呼び出すとかRPCのようだ.

サーバーサイドはこれでおしまい.

クライアントサイドの実装

クライアント側SignalRライブラリのインストール

クライアントサイドではJavaScriptでサーバーとSignalR通信を行う.

そのため,JavaScriptのSignalRライブラリが必要なのでインストールする.

@microsoft/signalrというnpmパッケージをインストールする.npmパッケージのインストールについては以下の記事を参考にする.

shuhelohelo.hatenablog.com

@microsoft/signalr 3.0.0をインストールする. f:id:shuhelohelo:20191006153728p:plain

(余談:@aspnet/signalrは古いパッケージ)

signalrのインストール後に以下のようなメッセージが表示されるかもしれないけれど,これはTypeScriptを使う場合のサジェストなので,今回は無視しておく.JavaScriptを使うので.

f:id:shuhelohelo:20191006154023p:plain

現時点でソリューションのフォルダ構成はこんな状態.

f:id:shuhelohelo:20191006154224p:plain

チャットページの実装

シンプルな作りにするために,wwwroot直下にindex.htmlを作成する.

index.htmlについての説明はコメントして書き込んだ.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <!-- 入力欄とボタン -->
    <input type="text" id="message" />
    <input type="button" value="Send" id="send" />

    <!-- ここにメッセージが追加されていく -->
    <ul id="messages"></ul>

    <!-- クライアント側のSignalRライブラリを読み込む -->
    <script src="lib/@microsoft/signalr/dist/browser/signalr.js"></script>

    <!-- サーバーにメッセージを送る処理 -->
    <script type="text/javascript">
        (async function () {

            var connection = new signalR.HubConnectionBuilder()
                .withUrl('/chathub')
                .build();

            //サーバーからのメッセージ受信時の処理
            connection.on('receive', function (message) { //サーバー側から呼び出されるメソッド名を文字列で
                //messageにサーバーからのデータが入ってくる
                //li要素を新規作成
                var li = document.createElement('li');

                //li要素のテキストとしてmessageをセットして追加
                li.innerText = message;
                document.getElementById('messages').appendChild(li);
            });

            //Sendボタンが押されたときの処理.Clickイベントにイベントハンドラを設定している.
            document.getElementById('send').addEventListener('click', async function () {
                //テキストボックス内の文字列を取得
                var value = document.getElementById('message').value;

                //サーバー側のSendメソッドにvalue(= message)を渡して実行する.
                //文字列でサーバー側のsendメソッドを指定する
                await connection.invoke('send', value);
            });

            await connection.start();
        })();
    </script>
</body>
</html>

これで完成.

実行

Ctrl+F5で実行する.

https://localhost:<<ポート番号>>/index.htmlにアクセスすると,以下のような味気のないシンプルなページが表示される.

f:id:shuhelohelo:20191006162434p:plain

同じページを複数開いて,片方で入力して送信したメッセージが全てのページに表示されることが確認できる.

f:id:shuhelohelo:20191006162653p:plain

注意点

HubのURLの文字列が同じであること.

//サーバー側
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapHub<ChatHub>("/chathub");//ここと
        });

//クライアント側
            var connection = new signalR.HubConnectionBuilder()
                .withUrl('/chathub')//ここ
                .build();

SignalRで呼び出すサーバー側メソッドは文字列で指定する.

例えば,ChatHubクラス内にSendメソッドを作ったとして,クライアント側からこのメソッドにメッセージを送ったり,このメソッド返す値を受け取る場合は,このメソッド名の文字列(この例では「send」.大文字小文字は無視される)を使う.

//サーバー側
    internal class ChatHub:Hub
    {
        public Task Send(string message)
        {
            return Clients.All.SendAsync("Send", message);
        }
    }

//クライアント側
            //サーバーからのメッセージ受信時の処理
            connection.on('send', function (message) { //****ここ***
                //messageにサーバーからのデータが入ってくる
                //li要素を新規作成
                var li = document.createElement('li');

                //li要素のテキストとしてmessageをセットして追加
                li.innerText = message;
                document.getElementById('messages').appendChild(li);
            });

            //Sendボタンが押されたときの処理.Clickイベントにイベントハンドラを設定している.
            document.getElementById('send').addEventListener('click', async function () { //****ここ***
                //テキストボックス内の文字列を取得
                var value = document.getElementById('message').value;

                //サーバー側のSendメソッドにvalue(= message)を渡して実行する.
                //文字列でサーバー側のメソッドを指定する
                await connection.invoke('send', value); //****ここ***
            });

ソースコード

このソースコードGitHubで公開している.

github.com