Tutorial: Serverübertragung mit ASP.NET SignalR 1.x

von Patrick Fletcher, Tom Dykstra

Warnung

Diese Dokumentation gilt nicht für die neueste Version von SignalR. Sehen Sie sich ASP.NET Core SignalR an.

In diesem Tutorial wird gezeigt, wie Sie eine Webanwendung erstellen, die ASP.NET SignalR verwendet, um Serverübertragungsfunktionen bereitzustellen. Serverübertragung bedeutet, dass die an Clients gesendeten Kommunikationen vom Server initiiert werden. Dieses Szenario erfordert einen anderen Programmieransatz als Peer-to-Peer-Szenarien wie Chatanwendungen, bei denen die an Clients gesendete Kommunikation von einem oder mehreren Clients initiiert wird.

Die Anwendung, die Sie in diesem Tutorial erstellen, simuliert einen Aktienticker, ein typisches Szenario für Serverübertragungsfunktionen.

Kommentare zum Tutorial sind willkommen. Wenn Sie Fragen haben, die nicht direkt mit dem Tutorial zusammenhängen, können Sie diese im ASP.NET SignalR-Forum oder StackOverflow.com posten.

Übersicht

Das NuGet-Paket Microsoft.AspNet.SignalR.Sample installiert eine simulierte Aktientickeranwendung in einem Visual Studio-Projekt. Im ersten Teil dieses Tutorials erstellen Sie eine vereinfachte Version dieser Anwendung von Grund auf neu. Im weiteren Verlauf des Tutorials installieren Sie das NuGet-Paket und überprüfen die zusätzlichen Features und den Code, den es erstellt.

Die Aktientickeranwendung ist ein Vertreter einer Art Echtzeitanwendung, in der Sie Benachrichtigungen vom Server in regelmäßigen Abständen an alle verbundenen Clients übertragen oder übertragen möchten.

Die Anwendung, die Sie im ersten Teil dieses Tutorials erstellen, zeigt ein Raster mit Bestandsdaten an.

StockTicker-Erstversion

In regelmäßigen Abständen aktualisiert der Server die Aktienkurse und pusht die Updates an alle verbundenen Clients. Im Browser ändern sich die Zahlen und Symbole in den Spalten Ändern und % dynamisch als Reaktion auf Benachrichtigungen vom Server. Wenn Sie zusätzliche Browser für dieselbe URL öffnen, werden alle gleichzeitig die gleichen Daten und die gleichen Änderungen an den Daten angezeigt.

Dieses Tutorial enthält die folgenden Abschnitte:

Hinweis

Wenn Sie die Schritte zum Erstellen der Anwendung nicht durcharbeiten möchten, können Sie das SignalR.Sample-Paket in einem neuen Projekt für eine leere ASP.NET-Webanwendung installieren und diese Schritte durchlesen, um Erklärungen zum Code zu erhalten. Der erste Teil des Tutorials behandelt eine Teilmenge des SignalR.Sample-Codes, und im zweiten Teil werden die wichtigsten Features der zusätzlichen Funktionalität im SignalR.Sample-Paket erläutert.

Voraussetzungen

Bevor Sie beginnen, stellen Sie sicher, dass Visual Studio 2012 oder 2010 SP1 auf Ihrem Computer installiert ist. Wenn Sie nicht über Visual Studio verfügen, lesen Sie ASP.NET Downloads , um das kostenlose Visual Studio 2012 Express for Web zu erhalten.

Wenn Sie Über Visual Studio 2010 verfügen, stellen Sie sicher, dass NuGet installiert ist.

Erstellen des Projekts

  1. Klicken Sie im Menü Datei auf Neues Projekt.

  2. Erweitern Sie im Dialogfeld Neues Projekt unter Vorlagenden Eintrag C#, und wählen Sie Web aus.

  3. Wählen Sie die Vorlage ASP.NET Leere Webanwendung aus, nennen Sie das Projekt SignalR.StockTicker, und klicken Sie auf OK.

    Dialogfeld

Hinzufügen der SignalR NuGet-Pakete

Hinzufügen der NuGet-Pakete SignalR und JQuery

Sie können signalR-Funktionalität zu einem Projekt hinzufügen, indem Sie ein NuGet-Paket installieren.

  1. Klicken Sie auf Extras | NuGet-Paket-Manager | Paket-Manager-Konsole.

  2. Geben Sie den folgenden Befehl im Paket-Manager ein.

    Install-Package Microsoft.AspNet.SignalR -Version 1.1.3
    

    Das SignalR-Paket installiert eine Reihe anderer NuGet-Pakete als Abhängigkeiten. Nach Abschluss der Installation verfügen Sie über alle Server- und Clientkomponenten, die für die Verwendung von SignalR in einer ASP.NET-Anwendung erforderlich sind.

Einrichten des Servercodes

In diesem Abschnitt richten Sie den Code ein, der auf dem Server ausgeführt wird.

Erstellen der Stock-Klasse

Sie erstellen zunächst die Stock-Modellklasse, die Sie zum Speichern und Übertragen von Informationen zu einem Bestand verwenden.

  1. Erstellen Sie eine neue Klassendatei im Projektordner, nennen Sie sie Stock.cs, und ersetzen Sie dann den Vorlagencode durch den folgenden Code:

    using System;
    
    namespace SignalR.StockTicker
    {
        public class Stock
        {
            private decimal _price;
    
            public string Symbol { get; set; }
    
            public decimal Price
            {
                get
                {
                    return _price;
                }
                set
                {
                    if (_price == value)
                    {
                        return;
                    }
    
                    _price = value;
    
                    if (DayOpen == 0)
                    {
                        DayOpen = _price;
                    }
                }
            }
    
            public decimal DayOpen { get; private set; }
    
            public decimal Change
            {
                get
                {
                    return Price - DayOpen;
                }
            }
    
            public double PercentChange
            {
                get
                {
                    return (double)Math.Round(Change / Price, 4);
                }
            }
        }
    }
    

    Die beiden Eigenschaften, die Sie beim Erstellen von Aktien festlegen, sind das Symbol (z. B. MSFT für Microsoft) und der Price. Die anderen Eigenschaften hängen davon ab, wie und wann Sie Den Preis festlegen. Wenn Sie Den Preis zum ersten Mal festlegen, wird der Wert an DayOpen weitergegeben. Nachfolgende Zeiten, in denen Sie Price festlegen, werden die Werte der Change- und PercentChange-Eigenschaft basierend auf der Differenz zwischen Price und DayOpen berechnet.

Erstellen der StockTicker- und StockTickerHub-Klassen

Sie verwenden die SignalR Hub-API, um die Server-zu-Client-Interaktion zu verarbeiten. Eine StockTickerHub-Klasse, die von der SignalR Hub-Klasse abgeleitet ist, verarbeitet den Empfang von Verbindungen und Methodenaufrufen von Clients. Außerdem müssen Sie Bestandsdaten verwalten und ein Timer-Objekt ausführen, um regelmäßig Preisaktualisierungen unabhängig von Clientverbindungen auszulösen. Sie können diese Funktionen nicht in einer Hub-Klasse platzieren, da Hubinstanzen vorübergehend sind. Eine Hubklasse instance wird für jeden Vorgang auf dem Hub erstellt, z. B. Verbindungen und Aufrufe vom Client an den Server. Daher muss der Mechanismus, der Aktiendaten speichert, Preise aktualisiert und die Preisupdates überträgt, in einer separaten Klasse ausgeführt werden, die Sie StockTicker nennen.

Übertragung aus StockTicker

Sie möchten nur eine instance der StockTicker-Klasse auf dem Server ausführen. Daher müssen Sie einen Verweis von jedem StockTickerHub-instance auf die Singleton StockTicker-instance einrichten. Die StockTicker-Klasse muss in der Lage sein, an Clients zu senden, da sie die Bestandsdaten enthält und Updates auslöst, aber StockTicker ist keine Hubklasse. Daher muss die StockTicker-Klasse einen Verweis auf das SignalR Hub-Verbindungskontextobjekt abrufen. Anschließend kann das SignalR-Verbindungskontextobjekt zum Senden an Clients verwendet werden.

  1. Klicken Sie in Projektmappen-Explorer mit der rechten Maustaste auf das Projekt, und klicken Sie dann auf Neues Element hinzufügen.

  2. Wenn Sie Über Visual Studio 2012 mit dem ASP.NET and Web Tools 2012.2 Update verfügen, klicken Sie unter Visual C# auf Web, und wählen Sie die Elementvorlage SignalR Hub-Klasse aus. Wählen Sie andernfalls die Klasse-Vorlage aus.

  3. Nennen Sie die neue Klasse StockTickerHub.cs, und klicken Sie dann auf Hinzufügen.

    Hinzufügen von StockTickerHub.cs

  4. Ersetzen Sie den Vorlagencode durch den folgenden Code:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    
    namespace SignalR.StockTicker
    {
        [HubName("stockTickerMini")]
        public class StockTickerHub : Hub
        {
            private readonly StockTicker _stockTicker;
    
            public StockTickerHub() : this(StockTicker.Instance) { }
    
            public StockTickerHub(StockTicker stockTicker)
            {
                _stockTicker = stockTicker;
            }
    
            public IEnumerable<Stock> GetAllStocks()
            {
                return _stockTicker.GetAllStocks();
            }
        }
    }
    

    Die Hub-Klasse wird verwendet, um Methoden zu definieren, die die Clients auf dem Server aufrufen können. Sie definieren eine Methode: GetAllStocks(). Wenn ein Client zunächst eine Verbindung mit dem Server herstellt, ruft er diese Methode auf, um eine Liste aller Bestände mit ihren aktuellen Preisen abzurufen. Die -Methode kann synchron ausgeführt und zurückgegeben IEnumerable<Stock> werden, da sie Daten aus dem Arbeitsspeicher zurückgibt. Wenn die Methode die Daten abrufen müsste, indem Sie etwas ausführen, das warten würde, z. B. eine Datenbanksuche oder einen Webdienstaufruf, würden Sie als Rückgabewert angeben Task<IEnumerable<Stock>> , um die asynchrone Verarbeitung zu aktivieren. Weitere Informationen finden Sie unter ASP.NET SignalR Hubs API Guide – Server – Wann asynchron ausgeführt werden soll.

    Das HubName-Attribut gibt an, wie auf den Hub im JavaScript-Code auf dem Client verwiesen wird. Der Standardname auf dem Client, wenn Sie dieses Attribut nicht verwenden, ist eine Camel-Case-Version des Klassennamens, der in diesem Fall stockTickerHub sein würde.

    Wie Sie später beim Erstellen der StockTicker-Klasse sehen werden, wird in der statischen Instanzeigenschaft eine Singleton-instance dieser Klasse erstellt. Dieser Singleton-instance von StockTicker verbleibt im Arbeitsspeicher, unabhängig davon, wie viele Clients eine Verbindung herstellen oder trennen, und dieser instance ist das, was die GetAllStocks-Methode verwendet, um aktuelle Bestandsinformationen zurückzugeben.

  5. Erstellen Sie eine neue Klassendatei im Projektordner, nennen Sie sie StockTicker.cs, und ersetzen Sie dann den Vorlagencode durch den folgenden Code:

    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Threading;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    
    namespace SignalR.StockTicker
    {
        public class StockTicker
        {
            // Singleton instance
            private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
    
            private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
    
            private readonly object _updateStockPricesLock = new object();
    
            //stock can go up or down by a percentage of this factor on each change
            private readonly double _rangePercent = .002;
    
            private readonly TimeSpan _updateInterval = TimeSpan.FromMilliseconds(250);
            private readonly Random _updateOrNotRandom = new Random();
    
            private readonly Timer _timer;
            private volatile bool _updatingStockPrices = false;
    
            private StockTicker(IHubConnectionContext clients)
            {
                Clients = clients;
    
                _stocks.Clear();
                var stocks = new List<Stock>
                {
                    new Stock { Symbol = "MSFT", Price = 30.31m },
                    new Stock { Symbol = "APPL", Price = 578.18m },
                    new Stock { Symbol = "GOOG", Price = 570.30m }
                };
                stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
    
                _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
    
            }
    
            public static StockTicker Instance
            {
                get
                {
                    return _instance.Value;
                }
            }
    
            private IHubConnectionContext Clients
            {
                get;
                set;
            }
    
            public IEnumerable<Stock> GetAllStocks()
            {
                return _stocks.Values;
            }
    
            private void UpdateStockPrices(object state)
            {
                lock (_updateStockPricesLock)
                {
                    if (!_updatingStockPrices)
                    {
                        _updatingStockPrices = true;
    
                        foreach (var stock in _stocks.Values)
                        {
                            if (TryUpdateStockPrice(stock))
                            {
                                BroadcastStockPrice(stock);
                            }
                        }
    
                        _updatingStockPrices = false;
                    }
                }
            }
    
            private bool TryUpdateStockPrice(Stock stock)
            {
                // Randomly choose whether to update this stock or not
                var r = _updateOrNotRandom.NextDouble();
                if (r > .1)
                {
                    return false;
                }
    
                // Update the stock price by a random factor of the range percent
                var random = new Random((int)Math.Floor(stock.Price));
                var percentChange = random.NextDouble() * _rangePercent;
                var pos = random.NextDouble() > .51;
                var change = Math.Round(stock.Price * (decimal)percentChange, 2);
                change = pos ? change : -change;
    
                stock.Price += change;
                return true;
            }
    
            private void BroadcastStockPrice(Stock stock)
            {
                Clients.All.updateStockPrice(stock);
            }
    
        }
    }
    

    Da mehrere Threads dieselbe instance von StockTicker-Code ausführen, muss die StockTicker-Klasse threadsicher sein.

    Speichern des Singleton-instance in einem statischen Feld

    Der Code initialisiert das statische _instance Feld, das die Instance-Eigenschaft mit einem instance der -Klasse unterstützt. Dies ist die einzige instance der Klasse, die erstellt werden kann, da der Konstruktor als privat gekennzeichnet ist. Die verzögerte Initialisierung wird für das Feld _instance verwendet, nicht aus Leistungsgründen, sondern um sicherzustellen, dass die instance Erstellung threadsicher ist.

    private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
    
    public static StockTicker Instance
    {
        get
        {
            return _instance.Value;
        }
    }
    

    Jedes Mal, wenn ein Client eine Verbindung mit dem Server herstellt, ruft eine neue instance der StockTickerHub-Klasse, die in einem separaten Thread ausgeführt wird, den StockTicker Singleton instance aus der statischen Eigenschaft StockTicker.Instance ab, wie Sie zuvor in der StockTickerHub-Klasse gesehen haben.

    Speichern von Bestandsdaten in einem ConcurrentDictionary

    Der Konstruktor initialisiert die _stocks-Auflistung mit einigen Beispieldaten, und GetAllStocks gibt die Bestände zurück. Wie Sie bereits gesehen haben, wird diese Sammlung von Aktien wiederum von StockTickerHub.GetAllStocks zurückgegeben, einer Servermethode in der Hub-Klasse, die von Clients aufgerufen werden kann.

    private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
    
    private StockTicker(IHubConnectionContext clients)
    {
        Clients = clients;
    
        _stocks.Clear();
        var stocks = new List<Stock>
        {
            new Stock { Symbol = "MSFT", Price = 30.31m },
            new Stock { Symbol = "APPL", Price = 578.18m },
            new Stock { Symbol = "GOOG", Price = 570.30m }
        };
        stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
    
        _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
    }
    
    public IEnumerable<Stock> GetAllStocks()
    {
        return _stocks.Values;
    }
    

    Die Bestandsauflistung ist für die Threadsicherheit als ConcurrentDictionary-Typ definiert. Alternativ können Sie ein Dictionary-Objekt verwenden und das Wörterbuch explizit sperren, wenn Sie Änderungen daran vornehmen.

    Für diese Beispielanwendung ist es in Ordnung, Anwendungsdaten im Arbeitsspeicher zu speichern und die Daten zu verlieren, wenn die StockTicker-instance entfernt wird. In einer realen Anwendung würden Sie mit einem Back-End-Datenspeicher wie einer Datenbank arbeiten.

    Regelmäßige Aktualisierung der Aktienkurse

    Der Konstruktor startet ein Timer-Objekt, das regelmäßig Methoden aufruft, die Aktienkurse nach dem Zufallsprinzip aktualisieren.

    _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
    
    private void UpdateStockPrices(object state)
    {
        lock (_updateStockPricesLock)
        {
            if (!_updatingStockPrices)
            {
                _updatingStockPrices = true;
    
                foreach (var stock in _stocks.Values)
                {
                    if (TryUpdateStockPrice(stock))
                    {
                        BroadcastStockPrice(stock);
                    }
                }
    
                _updatingStockPrices = false;
            }
        }
    }
    
    private bool TryUpdateStockPrice(Stock stock)
    {
        // Randomly choose whether to update this stock or not
        var r = _updateOrNotRandom.NextDouble();
        if (r > .1)
        {
            return false;
        }
    
        // Update the stock price by a random factor of the range percent
        var random = new Random((int)Math.Floor(stock.Price));
        var percentChange = random.NextDouble() * _rangePercent;
        var pos = random.NextDouble() > .51;
        var change = Math.Round(stock.Price * (decimal)percentChange, 2);
        change = pos ? change : -change;
    
        stock.Price += change;
        return true;
    }
    

    UpdateStockPrices wird vom Timer aufgerufen, der null im Zustandsparameter übergibt. Vor dem Aktualisieren der Preise wird eine Sperre für das _updateStockPricesLock-Objekt ausgeführt. Der Code überprüft, ob ein anderer Thread bereits Die Preise aktualisiert, und ruft dann TryUpdateStockPrice für jede Aktie in der Liste auf. Die TryUpdateStockPrice-Methode entscheidet, ob der Aktienkurs geändert werden soll und wie viel er geändert werden soll. Wenn der Aktienkurs geändert wird, wird BroadcastStockPrice aufgerufen, um die Aktienkursänderung an alle verbundenen Clients zu übertragen.

    Das _updatingStockPrices-Flag ist als volatil gekennzeichnet, um sicherzustellen, dass der Zugriff darauf threadsicher ist.

    private volatile bool _updatingStockPrices = false;
    

    In einer realen Anwendung ruft die TryUpdateStockPrice-Methode einen Webdienst auf, um den Preis nachzuschlagen. in diesem Code wird ein Zufallszahlengenerator verwendet, um änderungen nach dem Zufallsprinzip vorzunehmen.

    Abrufen des SignalR-Kontexts, damit die StockTicker-Klasse an Clients übertragen werden kann

    Da die Preisänderungen hier im StockTicker-Objekt entstehen, ist dies das Objekt, das eine updateStockPrice-Methode auf allen verbundenen Clients aufrufen muss. In einer Hub-Klasse verfügen Sie über eine API zum Aufrufen von Clientmethoden, aber StockTicker leitet sich nicht von der Hub-Klasse ab und verfügt über keinen Verweis auf ein Hub-Objekt. Um an verbundene Clients zu senden, muss die StockTicker-Klasse daher den SignalR-Kontext instance für die StockTickerHub-Klasse abrufen und diesen zum Aufrufen von Methoden auf Clients verwenden.

    Der Code ruft einen Verweis auf den SignalR-Kontext ab, wenn er die Singleton-Klasse instance erstellt, diesen Verweis an den Konstruktor übergibt, und der Konstruktor fügt ihn in die Clients-Eigenschaft ein.

    Es gibt zwei Gründe, warum Sie den Kontext nur einmal abrufen möchten: Das Abrufen des Kontexts ist ein teurer Vorgang, und das einmalige Abrufen stellt sicher, dass die beabsichtigte Reihenfolge der an Clients gesendeten Nachrichten beibehalten wird.

    private readonly static Lazy<StockTicker> _instance =
        new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
    
    private StockTicker(IHubConnectionContext clients)
    {
        Clients = clients;
    
        // Remainder of constructor ...
    }
    
    private IHubConnectionContext Clients
    {
        get;
        set;
    }
    
    private void BroadcastStockPrice(Stock stock)
    {
        Clients.All.updateStockPrice(stock);
    }
    

    Wenn Sie die Clients-Eigenschaft des Kontexts abrufen und in die StockTickerClient-Eigenschaft einfügen, können Sie Code schreiben, um Clientmethoden aufzurufen, die genauso aussehen wie in einer Hub-Klasse. Für instance können Sie Clients.All.updateStockPrice(stock) schreiben, um an alle Clients zu senden.

    Die updateStockPrice-Methode, die Sie in BroadcastStockPrice aufrufen, ist noch nicht vorhanden. Sie fügen es später hinzu, wenn Sie Code schreiben, der auf dem Client ausgeführt wird. Sie können hier auf updateStockPrice verweisen, da Clients.All dynamisch ist, was bedeutet, dass der Ausdruck zur Laufzeit ausgewertet wird. Wenn dieser Methodenaufruf ausgeführt wird, sendet SignalR den Methodennamen und den Parameterwert an den Client. Wenn der Client über eine Methode namens updateStockPrice verfügt, wird diese Methode aufgerufen und der Parameterwert an sie übergeben.

    Clients.All bedeutet Senden an alle Clients. SignalR bietet Ihnen weitere Optionen, um anzugeben, an welche Clients oder Clientgruppen gesendet werden sollen. Weitere Informationen finden Sie unter HubConnectionContext.

Registrieren der SignalR-Route

Der Server muss wissen, welche URL abgefangen und an SignalR weiterzuleiten ist. Dazu fügen Sie der Datei Global.asax Code hinzu.

  1. Klicken Sie in Projektmappen-Explorer mit der rechten Maustaste auf das Projekt, und klicken Sie dann auf Neues Element hinzufügen.

  2. Wählen Sie die Elementvorlage Globale Anwendungsklasse aus, und klicken Sie dann auf Hinzufügen.

    Hinzufügen von global.asax

  3. Fügen Sie der Application_Start-Methode den SignalR-Routenregistrierungscode hinzu:

    protected void Application_Start(object sender, EventArgs e)
    {
        RouteTable.Routes.MapHubs();
    }
    

    Standardmäßig lautet die Basis-URL für den gesamten SignalR-Datenverkehr "/signalr", und "/signalr/hubs" wird verwendet, um eine dynamisch generierte JavaScript-Datei abzurufen, die Proxys für alle Hubs in Ihrer Anwendung definiert. Die MapHubs-Methode enthält Überladungen, mit denen Sie eine andere Basis-URL und bestimmte SignalR-Optionen in einer instance der HubConfiguration-Klasse angeben können.

  4. Fügen Sie am Anfang der Datei eine using-Anweisung hinzu:

    using System.Web.Routing;
    
  5. Speichern und schließen Sie die Datei Global.asax , und erstellen Sie das Projekt.

Sie haben nun die Einrichtung des Servercodes abgeschlossen. Im nächsten Abschnitt richten Sie den Client ein.

Einrichten des Clientcodes

  1. Erstellen Sie eine neue HTML-Datei im Projektordner, und nennen Sie sie StockTicker.html.

  2. Ersetzen Sie den Vorlagencode durch den folgenden Code:

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>ASP.NET SignalR Stock Ticker</title>
        <style>
            body {
                font-family: 'Segoe UI', Arial, Helvetica, sans-serif;
                font-size: 16px;
            }
            #stockTable table {
                border-collapse: collapse;
            }
                #stockTable table th, #stockTable table td {
                    padding: 2px 6px;
                }
                #stockTable table td {
                    text-align: right;
                }
            #stockTable .loading td {
                text-align: left;
            }
        </style>
    </head>
    <body>
        <h1>ASP.NET SignalR Stock Ticker Sample</h1>
    
        <h2>Live Stock Table</h2>
        <div id="stockTable">
            <table border="1">
                <thead>
                    <tr><th>Symbol</th><th>Price</th><th>Open</th><th>Change</th><th>%</th></tr>
                </thead>
                <tbody>
                    <tr class="loading"><td colspan="5">loading...</td></tr>
                </tbody>
            </table>
        </div>
    
        <!--Script references. -->
        <!--Reference the jQuery library. -->
        <script src="/Scripts/jquery-1.8.2.min.js" ></script>
        <!--Reference the SignalR library. -->
        <script src="/Scripts/jquery.signalR-1.0.1.js"></script>
        <!--Reference the autogenerated SignalR hub script. -->
        <script src="/signalr/hubs"></script>
        <!--Reference the StockTicker script. -->
        <script src="StockTicker.js"></script>
    </body>
    </html>
    

    Der HTML-Code erstellt eine Tabelle mit 5 Spalten, eine Kopfzeile und eine Datenzeile mit einer einzelnen Zelle, die sich über alle 5 Spalten erstreckt. In der Datenzeile wird "Laden..." und wird nur vorübergehend angezeigt, wenn die Anwendung gestartet wird. JavaScript-Code entfernt diese Zeile und fügt an ihrer Stelle Zeilen mit vom Server abgerufenen Bestandsdaten hinzu.

    Die Skripttags geben die jQuery-Skriptdatei, die SignalR-Kernskriptdatei, die SignalR-Proxyskriptdatei und eine StockTicker-Skriptdatei an, die Sie später erstellen werden. Die SignalR-Proxyskriptdatei, die die URL "/signalr/hubs" angibt, wird dynamisch generiert und definiert Proxymethoden für die Methoden in der Hub-Klasse, in diesem Fall für StockTickerHub.GetAllStocks. Wenn Sie möchten, können Sie diese JavaScript-Datei manuell mithilfe von SignalR Utilities generieren und die Erstellung dynamischer Dateien im Aufruf der MapHubs-Methode deaktivieren.

  3. Wichtig

    Stellen Sie sicher, dass die JavaScript-Dateiverweise in StockTicker.html korrekt sind. Stellen Sie also sicher, dass die jQuery-Version in Ihrem Skripttag (im Beispiel 1.8.2) mit der jQuery-Version im Skriptordner Ihres Projekts identisch ist, und stellen Sie sicher, dass die SignalR-Version in Ihrem Skripttag mit der SignalR-Version im Skriptordner Ihres Projekts identisch ist. Ändern Sie bei Bedarf die Dateinamen in den Skripttags.

  4. Klicken Sie in Projektmappen-Explorer mit der rechten Maustaste auf StockTicker.html, und klicken Sie dann auf Als Startseite festlegen.

  5. Erstellen Sie eine neue JavaScript-Datei im Projektordner, und nennen Sie sie StockTicker.js..

  6. Ersetzen Sie den Vorlagencode durch den folgenden Code:

    // A simple templating method for replacing placeholders enclosed in curly braces.
    if (!String.prototype.supplant) {
        String.prototype.supplant = function (o) {
            return this.replace(/{([^{}]*)}/g,
                function (a, b) {
                    var r = o[b];
                    return typeof r === 'string' || typeof r === 'number' ? r : a;
                }
            );
        };
    }
    
    $(function () {
    
        var ticker = $.connection.stockTickerMini, // the generated client-side hub proxy
            up = '▲',
            down = '▼',
            $stockTable = $('#stockTable'),
            $stockTableBody = $stockTable.find('tbody'),
            rowTemplate = '<tr data-symbol="{Symbol}"><td>{Symbol}</td><td>{Price}</td><td>{DayOpen}</td><td>{Direction} {Change}</td><td>{PercentChange}</td></tr>';
    
        function formatStock(stock) {
            return $.extend(stock, {
                Price: stock.Price.toFixed(2),
                PercentChange: (stock.PercentChange * 100).toFixed(2) + '%',
                Direction: stock.Change === 0 ? '' : stock.Change >= 0 ? up : down
            });
        }
    
        function init() {
            ticker.server.getAllStocks().done(function (stocks) {
                $stockTableBody.empty();
                $.each(stocks, function () {
                    var stock = formatStock(this);
                    $stockTableBody.append(rowTemplate.supplant(stock));
                });
            });
        }
    
        // Add a client-side hub method that the server will call
        ticker.client.updateStockPrice = function (stock) {
            var displayStock = formatStock(stock),
                $row = $(rowTemplate.supplant(displayStock));
    
            $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
                .replaceWith($row);
            }
    
        // Start the connection
        $.connection.hub.start().done(init);
    
    });
    

    $.connection bezieht sich auf die SignalR-Proxys. Der Code ruft einen Verweis auf den Proxy für die StockTickerHub-Klasse ab und fügt ihn in die Tickervariable ein. Der Proxyname ist der Name, der vom [HubName]-Attribut festgelegt wurde:

    var ticker = $.connection.stockTickerMini
    
    [HubName("stockTickerMini")]
    public class StockTickerHub : Hub
    

    Nachdem alle Variablen und Funktionen definiert sind, initialisiert die letzte Codezeile in der Datei die SignalR-Verbindung, indem die SignalR-Startfunktion aufgerufen wird. Die Startfunktion wird asynchron ausgeführt und gibt ein jQuery Deferred-Objekt zurück. Dies bedeutet, dass Sie die done-Funktion aufrufen können, um die Funktion anzugeben, die aufgerufen werden soll, wenn der asynchrone Vorgang abgeschlossen ist.

    $.connection.hub.start().done(init);
    

    Die init-Funktion ruft die getAllStocks-Funktion auf dem Server auf und verwendet die vom Server zurückgegebenen Informationen, um die Bestandstabelle zu aktualisieren. Beachten Sie, dass Sie standardmäßig camel casing auf dem Client verwenden müssen, obwohl der Methodenname auf dem Server pascal-cased ist. Die Camel-Casing-Regel gilt nur für Methoden, nicht für Objekte. Sie verweisen z. B. auf Lagerbestand. Symbol und Bestand. Price, nicht stock.symbol oder stock.price.

    function init() {
        ticker.server.getAllStocks().done(function (stocks) {
            $stockTableBody.empty();
            $.each(stocks, function () {
                var stock = formatStock(this);
                $stockTableBody.append(rowTemplate.supplant(stock));
            });
        });
    }
    
    public IEnumerable<Stock> GetAllStocks()
    {
        return _stockTicker.GetAllStocks();
    }
    

    Wenn Sie die Pascal-Großschreibung auf dem Client verwenden möchten oder einen völlig anderen Methodennamen verwenden möchten, können Sie die Hub-Methode mit dem HubMethodName-Attribut auf die gleiche Weise wie die Hub-Klasse selbst mit dem HubName-Attribut versehen.

    In der init-Methode wird HTML für eine Tabellenzeile für jedes vom Server empfangene Stock-Objekt erstellt, indem formatStock aufgerufen wird, um Eigenschaften des Stock-Objekts zu formatieren, und dann durch Aufrufen von Supplant (das oben in StockTicker.jsdefiniert ist), um Platzhalter in der rowTemplate-Variable durch die Eigenschaftswerte des Stockobjekts zu ersetzen. Der resultierende HTML-Code wird dann an die Bestandstabelle angefügt.

    Sie rufen init auf, indem Sie es als Rückruffunktion übergeben, die nach Abschluss der asynchronen Startfunktion ausgeführt wird. Wenn Sie init nach dem Aufrufen von start als separate JavaScript-Anweisung aufgerufen haben, schlägt die Funktion fehl, da sie sofort ausgeführt wird, ohne darauf zu warten, dass die Startfunktion den Verbindungsaufbau abgeschlossen hat. In diesem Fall versucht die init-Funktion, die getAllStocks-Funktion aufzurufen, bevor die Serververbindung hergestellt wird.

    Wenn der Server den Preis einer Aktie ändert, ruft er den updateStockPrice auf verbundenen Clients auf. Die Funktion wird der Clienteigenschaft des stockTicker-Proxys hinzugefügt, um sie für Aufrufe vom Server verfügbar zu machen.

    ticker.client.updateStockPrice = function (stock) {
        var displayStock = formatStock(stock),
            $row = $(rowTemplate.supplant(displayStock));
    
        $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
            .replaceWith($row);
        }
    

    Die updateStockPrice-Funktion formatiert ein vom Server empfangenes Stockobjekt auf die gleiche Weise wie in der Init-Funktion in eine Tabellenzeile. Anstatt jedoch die Zeile an die Tabelle anzufügen, sucht sie die aktuelle Zeile des Bestands in der Tabelle und ersetzt diese Zeile durch die neue Zeile.

Testen der Anwendung

  1. Drücken Sie F5, um die Anwendung im Debugmodus auszuführen.

    In der Lagertabelle wird zunächst das "Laden..." zeilen, dann werden nach einer kurzen Verzögerung die ersten Aktiendaten angezeigt, und dann beginnen sich die Aktienkurse zu ändern.

    Laden

    Erste Lagerbestandstabelle

    Bestandstabelle, die Änderungen vom Server empfängt

  2. Kopieren Sie die URL aus der Adressleiste des Browsers, und fügen Sie sie in ein oder mehrere neue Browserfenster ein.

    Die anfängliche Bestandsanzeige ist identisch mit dem ersten Browser, und Änderungen erfolgen gleichzeitig.

  3. Schließen Sie alle Browser, öffnen Sie einen neuen Browser, und wechseln Sie dann zur gleichen URL.

    Das StockTicker-Singletonobjekt wurde weiterhin auf dem Server ausgeführt, sodass die Anzeige der Lagertabelle zeigt, dass sich die Bestände weiterhin geändert haben. (Die anfängliche Tabelle mit null Änderungszahlen wird nicht angezeigt.)

  4. Schließen Sie den Browser.

Aktivieren der Protokollierung

SignalR verfügt über eine integrierte Protokollierungsfunktion, die Sie auf dem Client aktivieren können, um die Problembehandlung zu unterstützen. In diesem Abschnitt aktivieren Sie die Protokollierung und sehen Beispiele, die zeigen, wie Protokolle Ihnen mitteilen, welche der folgenden Transportmethoden SignalR verwendet:

Für jede Verbindung wählt SignalR die beste Transportmethode aus, die sowohl vom Server als auch vom Client unterstützt wird.

  1. Öffnen Sie StockTicker.js , und fügen Sie eine Codezeile hinzu, um die Protokollierung unmittelbar vor dem Code zu aktivieren, der die Verbindung am Ende der Datei initialisiert:

    // Start the connection
    $.connection.hub.logging = true;
    $.connection.hub.start().done(init);
    
  2. Drücken Sie F5, um das Projekt auszuführen.

  3. Öffnen Sie das Entwicklertoolsfenster Ihres Browsers, und wählen Sie die Konsole aus, um die Protokolle anzuzeigen. Möglicherweise müssen Sie die Seite aktualisieren, um die Protokolle des Signalrs anzuzeigen, der die Transportmethode für eine neue Verbindung aushandelt.

    Wenn Sie internet Explorer 10 auf Windows 8 (IIS 8) ausführen, ist die Transportmethode WebSockets.

    IE 10 IIS 8-Konsole

    Wenn Sie Internet Explorer 10 unter Windows 7 (IIS 7.5) ausführen, ist die Transportmethode iframe.

    IE 10-Konsole, IIS 7.5

    Installieren Sie in Firefox das Firebug-Add-In, um ein Konsolenfenster zu erhalten. Wenn Sie Firefox 19 auf Windows 8 (IIS 8) ausführen, ist die Transportmethode WebSockets.

    Firefox 19 IIS 8 Websockets

    Wenn Sie Firefox 19 unter Windows 7 (IIS 7.5) ausführen, handelt es sich bei der Transportmethode um vom Server gesendete Ereignisse.

    Firefox 19 IIS 7.5-Konsole

Installieren und Überprüfen des vollständigen StockTicker-Beispiels

Die StockTicker-Anwendung, die vom NuGet-Paket Microsoft.AspNet.SignalR.Sample installiert wird, enthält mehr Features als die vereinfachte Version, die Sie gerade von Grund auf neu erstellt haben. In diesem Abschnitt des Tutorials installieren Sie das NuGet-Paket und überprüfen die neuen Features und den Code, mit dem sie implementiert werden.

Installieren des NuGet-Pakets SignalR.Sample

  1. Klicken Sie in Projektmappen-Explorer mit der rechten Maustaste auf das Projekt, und klicken Sie dann auf NuGet-Pakete verwalten.

  2. Klicken Sie im Dialogfeld NuGet-Pakete verwalten auf Online, geben Sie signalR.Sample in das Feld Online suchen ein, und klicken Sie dann im Paket SignalR.Sample auf Installieren.

    Installieren des Pakets

  3. Kommentieren Sie in der Datei Global.asax die RouteTable.Routes.MapHubs(); Zeile, die Sie zuvor in der Application_Start-Methode hinzugefügt haben.

    Der Code in Global.asax wird nicht mehr benötigt, da das SignalR.Sample-Paket die SignalR-Route in der Datei App_Start/RegisterHubs.cs registriert:

    [assembly: WebActivator.PreApplicationStartMethod(typeof(SignalR.StockTicker.RegisterHubs), "Start")]
    
    namespace SignalR.StockTicker
    {
        public static class RegisterHubs
        {
            public static void Start()
            {
                // Register the default hubs route: ~/signalr/hubs
                RouteTable.Routes.MapHubs();
            }
        }
    }
    

    Die WebActivator-Klasse, auf die das Assembly-Attribut verweist, ist im NuGet-Paket WebActivatorEx enthalten, das als Abhängigkeit des SignalR.Sample-Pakets installiert wird.

  4. Erweitern Sie in Projektmappen-Explorer den Ordner SignalR.Sample, der durch Installieren des Pakets SignalR.Sample erstellt wurde.

  5. Klicken Sie im Ordner SignalR.Sample mit der rechten Maustaste auf StockTicker.html, und klicken Sie dann auf Als Startseite festlegen.

    Hinweis

    Durch die Installation des NuGet-Pakets SignalR.Sample kann die Version von jQuery im Ordner Skripts geändert werden. Die neue StockTicker.html-Datei , die das Paket im Ordner SignalR.Sample installiert, wird mit der jQuery-Version synchronisiert, die das Paket installiert. Wenn Sie jedoch Ihre ursprüngliche StockTicker.html Datei erneut ausführen möchten, müssen Sie möglicherweise zuerst den jQuery-Verweis im Skripttag aktualisieren.

Ausführen der Anwendung

  1. Drücken Sie F5, um die Anwendung auszuführen.

    Zusätzlich zu dem Raster, das Sie zuvor gesehen haben, zeigt die vollständige Aktientickeranwendung ein horizontal scrollendes Fenster an, in dem dieselben Bestandsdaten angezeigt werden. Wenn Sie die Anwendung zum ersten Mal ausführen, ist der "Markt" geschlossen, und Es wird ein statisches Raster und ein Tickerfenster angezeigt, in dem kein Bildlauf ausgeführt wird.

    StockTicker-Bildschirmstart

    Wenn Sie auf Markt öffnen klicken, beginnt das Feld Live-Aktienticker horizontal zu scrollen, und der Server beginnt regelmäßig, Aktienkursänderungen nach dem Zufallsprinzip zu übertragen. Jedes Mal, wenn sich ein Aktienkurs ändert, werden sowohl das Raster Live Stock Table als auch das Feld Live Stock Ticker aktualisiert. Wenn die Kursänderung einer Aktie positiv ist, wird die Aktie mit grünem Hintergrund angezeigt, und wenn die Änderung negativ ist, wird die Aktie mit einem roten Hintergrund angezeigt.

    StockTicker-App, Markt geöffnet

    Die Schaltfläche Markt schließen stoppt die Änderungen und stoppt das Scrollen des Tickers, und die Schaltfläche Zurücksetzen setzt alle Aktiendaten auf den ursprünglichen Zustand zurück, bevor die Preisänderungen begonnen haben. Wenn Sie weitere Browserfenster öffnen und dieselbe URL aufrufen, werden dieselben Daten dynamisch gleichzeitig in jedem Browser aktualisiert. Wenn Sie auf eine der Schaltflächen klicken, reagieren alle Browser gleichzeitig auf die gleiche Weise.

Live-Aktientickeranzeige

Die Live Stock Ticker-Anzeige ist eine nicht sortierte Liste in einem div-Element, das nach CSS-Formatvorlagen in eine einzelne Zeile formatiert ist. Der Ticker wird wie die Tabelle initialisiert und aktualisiert: Durch Ersetzen von Platzhaltern in einer <li-Vorlagenzeichenfolge> und dynamisches Hinzufügen der <li-Elemente> zum <ul-Element> . Der Bildlauf wird mithilfe der jQuery-Animate-Funktion ausgeführt, um den linken Rand der nicht sortierten Liste innerhalb des div zu variieren.

Der Aktienticker-HTML:

<h2>Live Stock Ticker</h2>
<div id="stockTicker">
    <div class="inner">
        <ul>
            <li class="loading">loading...</li>
        </ul>
    </div>
</div>

Der Aktienticker CSS:

#stockTicker {
    overflow: hidden;
    width: 450px;
    height: 24px;
    border: 1px solid #999;
    }

    #stockTicker .inner {
        width: 9999px;
    }

    #stockTicker ul {
        display: inline-block;
        list-style-type: none;
        margin: 0;
        padding: 0;
    }

    #stockTicker li {
        display: inline-block;
        margin-right: 8px;   
    }

    /*<li data-symbol="{Symbol}"><span class="symbol">{Symbol}</span><span class="price">{Price}</span><span class="change">{PercentChange}</span></li>*/
    #stockTicker .symbol {
        font-weight: bold;
    }

    #stockTicker .change {
        font-style: italic;
    }

Der jQuery-Code, der den Bildlauf ermöglicht:

function scrollTicker() {
    var w = $stockTickerUl.width();
    $stockTickerUl.css({ marginLeft: w });
    $stockTickerUl.animate({ marginLeft: -w }, 15000, 'linear', scrollTicker);
}

Zusätzliche Methoden auf dem Server, die der Client aufrufen kann

Die StockTickerHub-Klasse definiert vier zusätzliche Methoden, die der Client aufrufen kann:

public string GetMarketState()
{
    return _stockTicker.MarketState.ToString();
}

public void OpenMarket()
{
    _stockTicker.OpenMarket();
}

public void CloseMarket()
{
    _stockTicker.CloseMarket();
}

public void Reset()
{
    _stockTicker.Reset();
}

OpenMarket, CloseMarket und Reset werden als Reaktion auf die Schaltflächen am oberen Rand der Seite aufgerufen. Sie veranschaulichen das Muster eines Clients, der eine Zustandsänderung auslöst, die sofort an alle Clients weitergegeben wird. Jede dieser Methoden ruft eine Methode in der StockTicker-Klasse auf, die sich auf die Änderung des Marktzustands auswirkt und dann den neuen Zustand sendet.

In der StockTicker-Klasse wird der Zustand des Marktes von einer MarketState-Eigenschaft beibehalten, die einen MarketState-Enumerationswert zurückgibt:

public MarketState MarketState
{
    get { return _marketState; }
    private set { _marketState = value; }
}

public enum MarketState
{
    Closed,
    Open
}

Jede der Methoden, die den Marktstatus ändern, tun dies innerhalb eines Sperrblocks, da die StockTicker-Klasse threadsicher sein muss:

public void OpenMarket()
{
    lock (_marketStateLock)
    {
        if (MarketState != MarketState.Open)
        {
            _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
            MarketState = MarketState.Open;
            BroadcastMarketStateChange(MarketState.Open);
        }
    }
}

public void CloseMarket()
{
    lock (_marketStateLock)
    {
        if (MarketState == MarketState.Open)
        {
            if (_timer != null)
            {
                _timer.Dispose();
            }
            MarketState = MarketState.Closed;
            BroadcastMarketStateChange(MarketState.Closed);
        }
    }
}

public void Reset()
{
    lock (_marketStateLock)
    {
        if (MarketState != MarketState.Closed)
        {
            throw new InvalidOperationException("Market must be closed before it can be reset.");
        }
        LoadDefaultStocks();
        BroadcastMarketReset();
    }
}

Um sicherzustellen, dass dieser Code threadsicher ist, wird das _marketState Feld, das die MarketState-Eigenschaft sichert, als volatil markiert.

private volatile MarketState _marketState;

Die Methoden BroadcastMarketStateChange und BroadcastMarketReset ähneln der BroadcastStockPrice-Methode, die Sie bereits gesehen haben, mit der Ausnahme, dass sie verschiedene Methoden aufrufen, die auf dem Client definiert sind:

private void BroadcastMarketStateChange(MarketState marketState)
{
    switch (marketState)
    {
        case MarketState.Open:
            Clients.All.marketOpened();
            break;
        case MarketState.Closed:
            Clients.All.marketClosed();
            break;
        default:
            break;
    }
}

private void BroadcastMarketReset()
{
    Clients.All.marketReset();
}

Zusätzliche Funktionen auf dem Client, die der Server aufrufen kann

Die Funktion updateStockPrice verarbeitet jetzt sowohl das Raster als auch die Tickeranzeige und verwendet jQuery.Color, um rote und grüne Farben zu blinken.

Neue Funktionen in SignalR.StockTicker.js die Schaltflächen basierend auf dem Marktzustand aktivieren und deaktivieren, und sie beenden oder starten das horizontale Scrollen des Tickerfensters. Da zu ticker.client mehrere Funktionen hinzugefügt werden, wird die jQuery-Erweiterungsfunktion verwendet, um sie hinzuzufügen.

$.extend(ticker.client, {
    updateStockPrice: function (stock) {
        var displayStock = formatStock(stock),
            $row = $(rowTemplate.supplant(displayStock)),
            $li = $(liTemplate.supplant(displayStock)),
            bg = stock.LastChange === 0
                ? '255,216,0' // yellow
                : stock.LastChange > 0
                    ? '154,240,117' // green
                    : '255,148,148'; // red

        $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
            .replaceWith($row);
        $stockTickerUl.find('li[data-symbol=' + stock.Symbol + ']')
            .replaceWith($li);

        $row.flash(bg, 1000);
        $li.flash(bg, 1000);
    },

    marketOpened: function () {
        $("#open").prop("disabled", true);
        $("#close").prop("disabled", false);
        $("#reset").prop("disabled", true);
        scrollTicker();
    },

    marketClosed: function () {
        $("#open").prop("disabled", false);
        $("#close").prop("disabled", true);
        $("#reset").prop("disabled", false);
        stopTicker();
    },

    marketReset: function () {
        return init();
    }
});

Zusätzliche Clienteinrichtung nach dem Herstellen der Verbindung

Nachdem der Client die Verbindung hergestellt hat, muss er einige zusätzliche Aufgaben erledigen: Ermitteln, ob der Markt geöffnet oder geschlossen ist, um die entsprechende marketOpened- oder marketClosed-Funktion aufzurufen, und die Aufrufe der Servermethode an die Schaltflächen anfügen.

$.connection.hub.start()
    .pipe(init)
    .pipe(function () {
        return ticker.server.getMarketState();
    })
    .done(function (state) {
        if (state === 'Open') {
            ticker.client.marketOpened();
        } else {
            ticker.client.marketClosed();
        }

        // Wire up the buttons
        $("#open").click(function () {
            ticker.server.openMarket();
        });

        $("#close").click(function () {
            ticker.server.closeMarket();
        });

        $("#reset").click(function () {
            ticker.server.reset();
        });
    });

Die Servermethoden werden erst nach dem Herstellen der Verbindung mit den Schaltflächen verkabelt, sodass der Code nicht versuchen kann, die Servermethoden aufzurufen, bevor sie verfügbar sind.

Nächste Schritte

In diesem Tutorial haben Sie gelernt, wie Sie eine SignalR-Anwendung programmieren, die Nachrichten vom Server an alle verbundenen Clients sendet, sowohl regelmäßig als auch als Reaktion auf Benachrichtigungen von einem beliebigen Client. Das Muster der Verwendung eines Multithread-Singleton-instance zur Aufrechterhaltung des Serverstatus kann auch in Onlinespielszenarien mit mehreren Spielern verwendet werden. Ein Beispiel finden Sie im ShootR-Spiel, das auf SignalR basiert.

Tutorials zu Peer-to-Peer-Kommunikationsszenarien finden Sie unter Erste Schritte mit SignalR und Echtzeitaktualisierung mit SignalR.

Weitere Informationen zu erweiterten SignalR-Entwicklungskonzepten finden Sie auf den folgenden Websites für SignalR-Quellcode und -Ressourcen: