From 5a2eabee4cee903d529bc317d03b45146f72d38f Mon Sep 17 00:00:00 2001 From: Sivaranjani Rajasekaran Date: Wed, 8 Apr 2026 10:53:21 +0530 Subject: [PATCH 1/6] 1014281 - added signalR sample --- .../ReadMe.md | 99 ++++ .../Controllers/StockController.cs | 211 +++++++ .../Controllers/WeatherForecastController.cs | 33 ++ .../SignalR.Server/Hubs/StockHub.cs | 37 ++ .../SignalR.Server/Models/Stock.cs | 555 ++++++++++++++++++ .../SignalR.Server/Program.cs | 47 ++ .../Properties/launchSettings.json | 45 ++ .../Services/StockDataService.cs | 12 + .../Services/StockUpdateService.cs | 85 +++ .../SignalR.Server/SignalR.Server.csproj | 29 + .../SignalR.Server/SignalR.Server.csproj.user | 6 + .../SignalR.Server/SignalR.Server.http | 6 + .../SignalR.Server/WeatherForecast.cs | 13 + .../appsettings.Development.json | 8 + .../SignalR.Server/appsettings.json | 9 + .../SignalR.sln | 33 ++ .../signalr.client/.editorconfig | 16 + .../signalr.client/.gitignore | 42 ++ .../signalr.client/angular.json | 113 ++++ .../signalr.client/aspnetcore-https.js | 37 ++ .../signalr.client/karma.conf.js | 44 ++ .../signalr.client/package.json | 47 ++ .../signalr.client/proxy.conf.json | 13 + .../signalr.client/signalr.client.esproj | 10 + .../signalr.client/src/app/app.component.css | 222 +++++++ .../signalr.client/src/app/app.component.html | 30 + .../src/app/app.component.spec.ts | 45 ++ .../signalr.client/src/app/app.component.ts | 157 +++++ .../signalr.client/src/assets/.gitkeep | 0 .../signalr.client/src/favicon.ico | Bin 0 -> 15086 bytes .../signalr.client/src/index.html | 14 + .../signalr.client/src/main.ts | 13 + .../signalr.client/src/proxy.conf.js | 16 + .../signalr.client/src/styles.css | 9 + .../signalr.client/tsconfig.app.json | 14 + .../signalr.client/tsconfig.json | 32 + .../signalr.client/tsconfig.spec.json | 14 + 37 files changed, 2116 insertions(+) create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/ReadMe.md create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/StockController.cs create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/WeatherForecastController.cs create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Hubs/StockHub.cs create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Models/Stock.cs create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Program.cs create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Properties/launchSettings.json create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Services/StockDataService.cs create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Services/StockUpdateService.cs create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.csproj create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.csproj.user create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.http create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/WeatherForecast.cs create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/appsettings.Development.json create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/appsettings.json create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.sln create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.editorconfig create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.gitignore create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/angular.json create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/aspnetcore-https.js create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/karma.conf.js create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/package.json create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/proxy.conf.json create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/signalr.client.esproj create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.css create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.html create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.spec.ts create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.ts create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/assets/.gitkeep create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/favicon.ico create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/index.html create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/main.ts create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/proxy.conf.js create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/styles.css create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.app.json create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.json create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.spec.json diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/ReadMe.md b/connecting-to-backends/syncfusion-reactgrid-with-signalr/ReadMe.md new file mode 100644 index 0000000..7585ad9 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/ReadMe.md @@ -0,0 +1,99 @@ +# Syncfusion Angular Grid with SignalR + +The Syncfusion Angular Grid component supports real-time data binding using SignalR, enabling automatic grid updates as data changes on the server. This capability proves essential for applications requiring live updates and multi-client synchronization. + +## Key Features + +- **Real-Time Communication**: Establish persistent connections for instant data updates across all connected clients. +- **Bidirectional**: Support both server-to-client (broadcasting) and client-to-server (commands) communication. +- **Automatic Transport Selection**: Intelligently choose the best transport protocol (WebSockets, SSE, Long Polling) based on browser and server capabilities. +- **Scalable Broadcasting**: Efficiently broadcast updates to multiple clients simultaneously using SignalR groups. +- **Built-in Reconnection**: Automatically handles client reconnection with exponential back off retry logic. +- **No Page Refresh Required**: Update UI dynamically without reloading the page. +Cross-Platform: Works across browsers, mobile devices, and desktop applications. + +## Prerequisites + + +| **Software / Package** | **Recommended version** | **Purpose** | +|-----------------------------|------------------------------|-------------------------------------- | +| Node.js | 20.x LTS or later | Runtime | +| npm / yarn / pnpm | 9.x or later | Package manager | +| Angular CLI | 17.x | Build and serve the Angular client | +| TypeScript | 5.x or later | Server‑side and client‑side type safety | + +## Quick Start + +1. **Clone the repository** + + ```bash + git clone + ``` + +2. **Running the application** + +**Run the Server:** + +- Run the below commands to run the server. + + ```bash + cd SignalR.Server + dotnet run + ``` +- The server runs at **http://localhost:5083/** by default. + +**Run the client** + + - Execute the below commands to run the client application. + + ```bash + cd signalr.client + npm install + npm run dev + ``` +- Open **https://127.0.0.1:58982/** in the browser. + + +## Project Layout + +| **File/Folder** | **Purpose** | +|-------------|---------| +| `signalr.client/package.json` | Client package manifest and dev/start scripts (`npm start`, `npm run dev`). | +| `signalr.client/src/main.ts` | Angular standalone bootstrap (bootstraps `AppComponent` with providers). | +| `signalr.client/src/app/app.component.ts` | Client grid logic (DataManager, SignalR hub connection, `setCellValue` updates). | +| `signalr.client/src/app/app.component.html` | Grid markup and column definitions for the Syncfusion grid. | +| `signalr.client/src/app/app.component.css` | Client UI styles and cell styling classes. | +| `SignalR.Server/` | ASP.NET Core backend with SignalR hubs, APIs, and background update service. | +| `SignalR.Server/Program.cs` | Server startup: DI, SignalR, controllers, CORS, and hub mappings (`/stockHub`). | +| `SignalR.Server/Controllers/StockController.cs` | Syncfusion `UrlDatasource` endpoint and other REST endpoints (GetAll, GetById, statistics). | +| `SignalR.Server/Hubs/StockHub.cs` | SignalR hub that sends `InitializeStocks` and manages subscriptions. | +| `SignalR.Server/Models/Stock.cs` | Server `Stock` model including raw values and `*Display` formatted fields. | +| `SignalR.Server/Services/StockUpdateService.cs` | Background service that simulates price updates and broadcasts them to the `StockTraders` group. | + + +## Common Tasks + +### Search / Filter / Sort +- Use the **Search** box (toolbar) to match across configured columns +- Use column filter icons for equals/contains/date filters +- Click column headers to sort ascending/descending + +## Steps to download GitHub samples using DownGit + +1. **Open the DownGit Website** + + Go to the official DownGit tool: https://downgit.github.io/#/home + +2. **Copy the GitHub URL** + + - Navigate to the sample folder you want to download and copy its URL. + - Example : https://github.com/SyncfusionExamples/ej2-angular-grid-samples/tree/master/connecting-to-backends/syncfusion-angular-grid-apollo-server + +3. **Paste the URL into DownGit** + + In the DownGit input box, paste the copied GitHub URL. + +4. **Download the ZIP** + + - Click **Download**. + - DownGit will generate a ZIP file of the selected folder, which you can save and extract locally. \ No newline at end of file diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/StockController.cs b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/StockController.cs new file mode 100644 index 0000000..f8a46f7 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/StockController.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SignalR.Server.Models; +using Microsoft.AspNetCore.Mvc; +using Syncfusion.EJ2.Base; +using System.Collections; + +namespace SignalR.Server.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class StockController : ControllerBase + { + /// + /// Fetch stock data with support for Syncfusion DataManager operations + /// Supports: Search, Sorting, Filtering, Paging + /// + [HttpPost("UrlDatasource")] + public IActionResult UrlDatasource([FromBody] DataManagerRequest dm) + { + try + { + // Get all stocks from the static collection + IEnumerable DataSource = Stock.GetAllStocks().ToList(); + DataOperations operation = new DataOperations(); + + // Search operation + if (dm.Search != null && dm.Search.Count > 0) + { + DataSource = operation.PerformSearching(DataSource, dm.Search); + } + + // Sorting operation + if (dm.Sorted != null && dm.Sorted.Count > 0) + { + DataSource = operation.PerformSorting(DataSource, dm.Sorted); + } + + // Filtering operation + if (dm.Where != null && dm.Where.Count > 0) + { + DataSource = operation.PerformFiltering(DataSource, dm.Where, dm.Where[0].Operator); + } + + // Get total count before paging + int count = DataSource.Cast().Count(); + + // Paging operations + if (dm.Skip != 0) + { + DataSource = operation.PerformSkip(DataSource, dm.Skip); + } + + if (dm.Take != 0) + { + DataSource = operation.PerformTake(DataSource, dm.Take); + } + + // Return result with count if required + return dm.RequiresCounts + ? Ok(new { result = DataSource, count = count }) + : Ok(DataSource); + } + catch (Exception ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Get all stocks + /// + [HttpGet("GetAll")] + public IActionResult GetAll() + { + try + { + var stocks = Stock.GetAllStocks(); + return Ok(stocks); + } + catch (Exception ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Get a single stock by StockId + /// + [HttpGet("GetById/{id}")] + public IActionResult GetById(int id) + { + try + { + var stock = Stock.GetAllStocks().FirstOrDefault(s => s.StockId == id); + if (stock == null) + { + return NotFound(new { error = $"Stock with ID {id} not found" }); + } + return Ok(stock); + } + catch (Exception ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + /// + /// Get stocks by symbol (e.g., "AAPL") + /// + [HttpGet("GetBySymbol/{symbol}")] + public IActionResult GetBySymbol(string symbol) + { + try + { + var stock = Stock.GetAllStocks() + .FirstOrDefault(s => s.Symbol.Equals(symbol, StringComparison.OrdinalIgnoreCase)); + + if (stock == null) + { + return NotFound(new { error = $"Stock with symbol {symbol} not found" }); + } + return Ok(stock); + } + catch (Exception ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + + + /// + /// Get stock statistics (min, max, average price, etc.) + /// + [HttpGet("GetStatistics")] + public IActionResult GetStatistics() + { + try + { + var stocks = Stock.GetAllStocks(); + + if (stocks.Count == 0) + { + return NotFound(new { error = "No stocks available" }); + } + + var stats = new + { + totalStocks = stocks.Count, + minPrice = stocks.Min(s => s.CurrentPrice), + maxPrice = stocks.Max(s => s.CurrentPrice), + averagePrice = stocks.Average(s => s.CurrentPrice), + positiveChanges = stocks.Count(s => s.Change > 0), + negativeChanges = stocks.Count(s => s.Change < 0), + totalVolume = stocks.Sum(s => s.Volume), + averageChangePercent = stocks.Average(s => s.ChangePercent), + lastUpdated = DateTime.UtcNow + }; + + return Ok(stats); + } + catch (Exception ex) + { + return BadRequest(new { error = ex.Message }); + } + } + } + + /// + /// CRUD Model for handling Syncfusion DataManager CRUD operations + /// + public class CRUDModel where T : class + { + public string? action { get; set; } + public string? table { get; set; } + public string? keyColumn { get; set; } + public object? key { get; set; } + public T? value { get; set; } + public List? added { get; set; } + public List? changed { get; set; } + public List? deleted { get; set; } + public IDictionary? @params { get; set; } + } + + /// + /// Where clause model for filtering operations + /// + public class Wheres + { + public List? predicates { get; set; } + public string? field { get; set; } + public bool ignoreCase { get; set; } + public bool isComplex { get; set; } + public string? value { get; set; } + public string? Operator { get; set; } + } + + /// + /// Predicates model for complex filtering + /// + public class Predicates + { + public string? value { get; set; } + public string? field { get; set; } + public bool isComplex { get; set; } + public bool ignoreCase { get; set; } + public string? Operator { get; set; } + } +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/WeatherForecastController.cs b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/WeatherForecastController.cs new file mode 100644 index 0000000..ab4a5b1 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/WeatherForecastController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Mvc; + +namespace SignalR.Server.Controllers +{ + [ApiController] + [Route("[controller]")] + public class WeatherForecastController : ControllerBase + { + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } + } +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Hubs/StockHub.cs b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Hubs/StockHub.cs new file mode 100644 index 0000000..13918fe --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Hubs/StockHub.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.SignalR; +using SignalR.Server.Models; +using SignalR.Server.Services; + +namespace SignalR.Server.Hubs +{ + public class StockHub : Hub + { + private readonly StockDataService _stockDataService; + + public StockHub(StockDataService stockDataService) + { + _stockDataService = stockDataService; + } + + public override async Task OnConnectedAsync() + { + await base.OnConnectedAsync(); + var stocks = _stockDataService.GetAllStocks(); + await Clients.Client(Context.ConnectionId).SendAsync("InitializeStocks", stocks); + } + + public async Task SubscribeToStocks() + { + await Groups.AddToGroupAsync(Context.ConnectionId, "StockTraders"); + var stocks = _stockDataService.GetAllStocks(); + await Clients.Caller.SendAsync("InitializeStocks", stocks); + } + + public async Task UnsubscribeFromStocks() + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, "StockTraders"); + } + } +} + + diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Models/Stock.cs b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Models/Stock.cs new file mode 100644 index 0000000..86ff7fc --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Models/Stock.cs @@ -0,0 +1,555 @@ +using System; +using System.Collections.Generic; + +namespace SignalR.Server.Models +{ + public class Stock + { + /// + /// Gets or sets the unique identifier for the stock. + /// + public int StockId { get; set; } + + /// + /// Gets or sets the ticker symbol of the stock (e.g., AAPL, MSFT). + /// + public string Symbol { get; set; } = string.Empty; + + /// + /// Gets or sets the full company name. + /// + public string Company { get; set; } = string.Empty; + + /// + /// Gets or sets the current price of the stock. + /// + public decimal CurrentPrice { get; set; } + + /// + /// Gets or sets the previous price before the last update. + /// Used to calculate price changes. + /// + public decimal PreviousPrice { get; set; } + + /// + /// Gets or sets the price change in absolute value. + /// Calculated as CurrentPrice - PreviousPrice. + /// + public decimal Change { get; set; } + + /// + /// Gets or sets the percentage change of the stock price. + /// Calculated as (Change / PreviousPrice) * 100. + /// + public decimal ChangePercent { get; set; } + + /// + /// Gets or sets the trading volume (number of shares traded). + /// + public long Volume { get; set; } + + /// + /// Gets or sets the timestamp of the last price update. + /// + public DateTime LastUpdated { get; set; } + + /// + /// Formatted display value for current price (USD currency) + /// + public string CurrentPriceDisplay { get; set; } = string.Empty; + + /// + /// Formatted display value for price change + /// + public string ChangeDisplay { get; set; } = string.Empty; + + /// + /// Formatted display value for percentage change + /// + public string ChangePercentDisplay { get; set; } = string.Empty; + + /// + /// Formatted display value for trading volume + /// + public string VolumeDisplay { get; set; } = string.Empty; + + /// + /// Static collection of stocks + /// + public static readonly List Stocks = new List(); + + private static readonly Random _random = new Random(); + private static bool _initialized = false; + + /// + /// Static constructor to initialize stocks + /// + static Stock() + { + InitializeStocks(); + } + + private static void InitializeStocks() + { + var stockData = new[] + { + // Technology + ("AAPL", "Apple Inc.", 190.50m), + ("MSFT", "Microsoft Corporation", 380.25m), + ("GOOGL", "Alphabet Inc.", 140.75m), + ("AMZN", "Amazon.com Inc.", 180.50m), + ("NVDA", "NVIDIA Corporation", 870.20m), + ("META", "Meta Platforms Inc.", 520.15m), + ("TSLA", "Tesla Inc.", 242.80m), + ("CRM", "Salesforce Inc.", 285.40m), + ("ADBE", "Adobe Inc.", 520.60m), + ("INTC", "Intel Corporation", 32.15m), + ("AMD", "Advanced Micro Devices", 210.80m), + ("QCOM", "Qualcomm Inc.", 165.30m), + ("CSCO", "Cisco Systems Inc.", 48.20m), + ("AMAT", "Applied Materials Inc.", 220.75m), + ("LRCX", "Lam Research Corporation", 780.45m), + ("ASML", "ASML Holding N.V.", 720.20m), + ("AVGO", "Broadcom Inc.", 145.60m), + ("MU", "Micron Technology Inc.", 95.40m), + ("NXPI", "NXP Semiconductors", 210.15m), + ("MCHP", "Microchip Technology Inc.", 72.85m), + + // Financial Services + ("JPM", "JPMorgan Chase & Co.", 195.75m), + ("BAC", "Bank of America Corp.", 42.50m), + ("WFC", "Wells Fargo & Company", 70.20m), + ("GS", "The Goldman Sachs Group Inc.", 510.85m), + ("MS", "Morgan Stanley", 98.40m), + ("BLK", "BlackRock Inc.", 890.15m), + ("AXP", "American Express Company", 245.60m), + ("USB", "U.S. Bancorp", 45.75m), + ("PNC", "PNC Financial Services", 185.40m), + ("TD", "Toronto-Dominion Bank", 68.90m), + ("RY", "Royal Bank of Canada", 115.20m), + ("BNS", "Scotiabank", 72.50m), + ("BMO", "Bank of Montreal", 108.75m), + ("CM", "Canadian Imperial Bank", 58.40m), + ("SLF", "Sun Life Financial Inc.", 68.20m), + ("MFC", "Manulife Financial Corporation", 23.15m), + ("GWC", "Great-West Lifeco Inc.", 32.80m), + ("TRI", "Thomson Reuters Corporation", 165.45m), + ("RCI", "Rogers Communications Inc.", 44.75m), + ("BCE", "BCE Inc.", 40.20m), + + // Healthcare & Pharmaceuticals + ("JNJ", "Johnson & Johnson", 158.45m), + ("UNH", "UnitedHealth Group Incorporated", 495.80m), + ("PFE", "Pfizer Inc.", 28.60m), + ("ABBV", "AbbVie Inc.", 285.20m), + ("LLY", "Eli Lilly and Company", 745.30m), + ("MRK", "Merck & Co Inc.", 65.90m), + ("TMO", "Thermo Fisher Scientific Inc.", 525.75m), + ("AZN", "AstraZeneca PLC", 68.40m), + ("AMGN", "Amgen Inc.", 295.15m), + ("GILD", "Gilead Sciences Inc.", 78.85m), + ("REGN", "Regeneron Pharmaceuticals Inc.", 975.20m), + ("BIIB", "Biogen Inc.", 240.50m), + ("CVS", "CVS Health Corporation", 75.40m), + ("WBA", "Walgreens Boots Alliance Inc.", 28.75m), + ("HCA", "HCA Healthcare Inc.", 310.60m), + ("UHS", "Universal Health Services Inc.", 245.85m), + ("THC", "Tenet Healthcare Corporation", 95.20m), + ("VEEV", "Veeva Systems Inc.", 172.45m), + ("DXCM", "DexCom Inc.", 105.80m), + ("INTU", "Intuit Inc.", 685.25m), + + // Consumer Discretionary + ("AMZN", "Amazon.com Inc.", 180.50m), + ("TSLA", "Tesla Inc.", 242.80m), + ("MCD", "McDonald's Corporation", 298.75m), + ("NKE", "Nike Inc.", 82.40m), + ("HD", "The Home Depot Inc.", 418.20m), + ("LOW", "Lowe's Companies Inc.", 245.60m), + ("WMT", "Walmart Inc.", 92.50m), + ("TGT", "Target Corporation", 78.20m), + ("COST", "Costco Wholesale Corporation", 945.80m), + ("ROST", "Ross Stores Inc.", 102.35m), + ("DXY", "Dixie Brands", 12.40m), + ("GPS", "The Gap Inc.", 24.15m), + ("KSS", "Kohl's Corporation", 18.95m), + ("BBY", "Best Buy Co. Inc.", 95.60m), + ("GME", "GameStop Corp.", 28.75m), + ("DISH", "DISH Network Corporation", 18.40m), + ("F", "Ford Motor Company", 10.20m), + ("GM", "General Motors Company", 42.85m), + ("TM", "Toyota Motor Corporation", 185.40m), + ("HMC", "Honda Motor Company Inc.", 28.60m), + + // Industrials & Materials + ("BA", "The Boeing Company", 218.50m), + ("CAT", "Caterpillar Inc.", 385.20m), + ("GE", "General Electric Company", 172.40m), + ("MMM", "3M Company", 108.75m), + ("RTX", "Raytheon Technologies Corporation", 125.60m), + ("LMT", "Lockheed Martin Corporation", 485.80m), + ("NOC", "Northrop Grumman Corporation", 545.20m), + ("GD", "General Dynamics Corporation", 285.40m), + ("DOW", "Dow Inc.", 52.15m), + ("DD", "DuPont de Nemours Inc.", 78.85m), + ("FCX", "Freeport-McMoRan Inc.", 42.60m), + ("NUCOR", "Nucor Corporation", 182.75m), + ("CMC", "Commercial Metals Company", 48.20m), + ("X", "United States Steel Corporation", 38.45m), + ("AA", "Alcoa Corporation", 52.80m), + ("CLF", "Cleveland-Cliffs Inc.", 18.60m), + ("STLD", "Steel Dynamics Inc.", 72.40m), + ("LIN", "Linde PLC", 465.90m), + ("APD", "Air Products and Chemicals Inc.", 285.20m), + ("EMN", "Eastman Chemical Company", 78.50m), + + // Energy + ("XOM", "Exxon Mobil Corporation", 118.40m), + ("CVX", "Chevron Corporation", 165.80m), + ("COP", "ConocoPhillips", 125.20m), + ("SLB", "Schlumberger Limited", 55.40m), + ("EOG", "EOG Resources Inc.", 105.60m), + ("MPC", "Marathon Petroleum Corporation", 185.75m), + ("PSX", "Phillips 66", 125.40m), + ("VLO", "Valero Energy Corporation", 145.80m), + ("HES", "Hess Corporation", 165.20m), + ("PXD", "Pioneer Natural Resources", 245.60m), + ("OXY", "Occidental Petroleum Corporation", 62.80m), + ("APA", "APA Corporation", 28.40m), + ("DVN", "Devon Energy Corporation", 45.20m), + ("FANG", "Diamondback Energy Inc.", 165.85m), + ("MRO", "Marathon Oil Corporation", 22.60m), + ("NBL", "Noble Corporation PLC", 52.40m), + ("CIVI", "Civista Bancshares Inc.", 24.15m), + ("KMI", "Kinder Morgan Inc.", 28.75m), + ("ET", "Energy Transfer LP", 12.85m), + ("MMP", "Magellan Midstream Partners L.P.", 48.20m), + + // Utilities + ("NEE", "NextEra Energy Inc.", 68.40m), + ("DUK", "Duke Energy Corporation", 98.20m), + ("SO", "The Southern Company", 72.50m), + ("DTE", "DTE Energy Company", 125.80m), + ("EXC", "Exelon Corporation", 42.15m), + ("AEP", "American Electric Power Company Inc.", 88.60m), + ("XEL", "Xcel Energy Inc.", 65.40m), + ("PEG", "Public Service Enterprise Group Inc.", 68.75m), + ("ED", "Consolidated Edison Inc.", 85.20m), + ("AES", "The AES Corporation", 25.45m), + ("EIX", "Edison International", 78.40m), + ("AWK", "American Water Works Company Inc.", 155.80m), + ("CWT", "California Water Service Group", 48.20m), + ("AWH", "Aspire Water Holdings Inc.", 18.60m), + ("NRG", "NRG Energy Inc.", 38.75m), + ("CMS", "CMS Energy Corporation", 62.40m), + ("FE", "FirstEnergy Corp.", 38.85m), + ("LNT", "Alliant Energy Corporation", 58.20m), + ("WEC", "WEC Energy Group Inc.", 92.50m), + ("PPL", "PPL Corporation", 32.15m), + + // Real Estate + ("DLR", "Digital Realty Trust Inc.", 185.40m), + ("EQIX", "Equinix Inc.", 625.80m), + ("VICI", "VICI Properties Inc.", 38.20m), + ("PLD", "Prologis Inc.", 108.60m), + ("AMT", "American Tower Corporation", 265.20m), + ("CCI", "Crown Castle International Corp.", 125.40m), + ("SBAC", "SBA Communications Corporation", 398.75m), + ("SPG", "Simon Property Group Inc.", 185.50m), + ("PEI", "Pennsylvania REIT", 8.40m), + ("MAC", "Macerich Company", 18.60m), + ("BXP", "Boston Properties Inc.", 95.20m), + ("OFC", "Monmouth Real Estate Investment Corporation", 2.50m), + ("VNO", "Vornado Realty Trust", 38.45m), + ("RXP", "REX Real Estate Investment Trust", 18.75m), + ("ARE", "Alexandria Real Estate Equities Inc.", 145.80m), + ("WELL", "Welltower Inc.", 72.40m), + ("PTC", "PotlatchDeltic Corporation", 52.80m), + ("UMH", "UMH Properties Inc.", 22.60m), + ("SRC", "Sotherly Bank Inc.", 15.20m), + ("AKR", "Acadia Realty Trust", 18.95m), + + // Consumer Staples + ("PG", "The Procter & Gamble Company", 168.40m), + ("KO", "The Coca-Cola Company", 65.20m), + ("PEP", "PepsiCo Inc.", 195.80m), + ("MO", "Altria Group Inc.", 58.40m), + ("PM", "Philip Morris International Inc.", 105.60m), + ("GIS", "General Mills Inc.", 78.20m), + ("CAG", "Conagra Brands Inc.", 32.15m), + ("K", "Kellogg Company", 22.80m), + ("MNST", "Monster Beverage Corporation", 58.40m), + ("CELH", "Celsius Holdings Inc.", 35.60m), + ("BF/B", "Brown-Forman Corporation", 48.50m), + ("DEO", "Diageo PLC", 88.40m), + ("STZ", "Constellation Brands Inc.", 285.20m), + ("TAP", "Molson Coors Beverage Company", 48.60m), + ("SJM", "The J.M. Smucker Company", 142.45m), + ("TSN", "Tyson Foods Inc.", 38.80m), + ("JBS", "JBS S.A.", 28.40m), + ("HRL", "Hormel Foods Corporation", 52.75m), + ("SMPL", "Simply Goods Inc.", 18.60m), + ("AGRO", "Adecoagro S.A.", 8.40m), + + // Communications + ("CMCSA", "Comcast Corporation", 45.20m), + ("CHTR", "Charter Communications Inc.", 385.80m), + ("TWX", "Time Warner Inc.", 85.40m), + ("FOX", "Fox Corporation", 28.50m), + ("FOXA", "Fox Corporation Class A", 28.60m), + ("VIAC", "ViacomCBS Inc.", 18.40m), + ("DIS", "The Walt Disney Company", 95.20m), + ("NFLX", "Netflix Inc.", 285.40m), + ("ROKU", "Roku Inc.", 68.20m), + ("PENN", "Penn Entertainment Inc.", 22.85m), + ("LYV", "Live Nation Entertainment Inc.", 142.60m), + ("RCI", "Rogers Communications Inc.", 44.75m), + ("BCE", "BCE Inc.", 40.20m), + ("T", "AT&T Inc.", 22.50m), + ("VZ", "Verizon Communications Inc.", 42.80m), + ("TMUS", "T-Mobile US Inc.", 195.40m), + ("S", "Sprint Corporation", 5.20m), + ("DISH", "DISH Network Corporation", 18.40m), + ("SIRIUSX", "Sirius XM Holdings Inc.", 28.60m), + ("LBRDK", "Liberty Braves Group", 32.40m), + + // Additional Tech & Software + ("FTNT", "Fortinet Inc.", 68.20m), + ("PALO", "Palo Alto Networks Inc.", 285.40m), + ("NET", "Cloudflare Inc.", 95.80m), + ("CRWD", "CrowdStrike Holdings Inc.", 425.20m), + ("OKTA", "Okta Inc.", 118.40m), + ("ZS", "Zscaler Inc.", 142.60m), + ("WORK", "Slack Technologies Inc.", 48.20m), + ("ZOOM", "Zoom Video Communications Inc.", 165.80m), + ("TWLO", "Twilio Inc.", 52.40m), + ("RBLX", "Roblox Corporation", 42.15m), + ("SNAP", "Snap Inc.", 25.80m), + ("PINS", "Pinterest Inc.", 32.40m), + ("COIN", "Coinbase Global Inc.", 125.60m), + ("MSTR", "MicroStrategy Incorporated", 485.20m), + ("SQ", "Square Inc.", 68.40m), + ("PYPL", "PayPal Holdings Inc.", 78.20m), + ("V", "Visa Inc.", 295.40m), + ("MA", "Mastercard Incorporated", 518.80m), + ("DFS", "Discover Financial Services", 125.40m), + ("ACI", "Advance Auto Parts Inc.", 15.20m), + + // Additional Technology & Software + ("SPLK", "Splunk Inc.", 155.40m), + ("DDOG", "Datadog Inc.", 195.80m), + ("SNOW", "Snowflake Inc.", 185.20m), + ("DBX", "Dropbox Inc.", 42.60m), + ("CrowdStrike", "CrowdStrike Holdings Inc.", 425.20m), + ("NFLX", "Netflix Inc.", 285.40m), + ("ROKU", "Roku Inc.", 68.20m), + ("GTLB", "Gitlab Inc.", 78.40m), + ("MNDY", "Monday.com Ltd.", 195.60m), + ("SMCI", "Super Micro Computer Inc.", 68.40m), + + // Additional Healthcare + ("SGEN", "Seagen Inc.", 165.80m), + ("VEEV", "Veeva Systems Inc.", 172.45m), + ("EXAS", "Exact Sciences Corporation", 78.20m), + ("ILMN", "Illumina Inc.", 115.40m), + ("VRTX", "Vertex Pharmaceuticals Inc.", 485.20m), + ("ALKS", "Alkermes PLC", 42.60m), + ("SAGE", "Sage Therapeutics Inc.", 28.40m), + ("CARA", "Cara Therapeutics Inc.", 35.20m), + ("BNTX", "Biontech SE", 125.40m), + ("MRNA", "Moderna Inc.", 185.80m), + + // Additional Financial Services + ("SCHW", "Charles Schwab Corporation", 82.40m), + ("HOOD", "Robinhood Markets Inc.", 32.60m), + ("UPST", "Upstart Holdings Inc.", 48.20m), + ("SQ", "Square Inc.", 68.40m), + ("PYPL", "PayPal Holdings Inc.", 78.20m), + ("INFA", "Informatica Inc.", 38.60m), + ("SOFI", "SoFi Technologies Inc.", 22.80m), + ("COIN", "Coinbase Global Inc.", 125.60m), + ("MSTR", "MicroStrategy Incorporated", 485.20m), + ("MARA", "Marathon Digital Holdings Inc.", 18.40m), + + // Additional Consumer & Retail + ("ULTA", "Ulta Beauty Inc.", 428.60m), + ("GPC", "Genuine Parts Company", 185.20m), + ("FIVE", "Five Below Inc.", 52.40m), + ("AMTM", "Artisan Partners Asset Management", 65.80m), + ("LULU", "Lululemon Athletica Inc.", 385.40m), + ("DECK", "Deckers Outdoor Corporation", 795.20m), + ("CROX", "Crocs Inc.", 95.80m), + ("VIPS", "Vipshop Holdings Limited", 28.40m), + ("NU", "Nu Holdings Ltd.", 8.60m), + ("MGNX", "MagneX Holdings Inc.", 5.40m), + + // Additional Industrials & Manufacturing + ("RBLX", "Roblox Corporation", 42.15m), + ("PTON", "Peloton Interactive Inc.", 12.40m), + ("LCID", "Lucid Motors Inc.", 2.80m), + ("NIO", "NIO Inc.", 5.20m), + ("XP", "XP Inc.", 18.40m), + ("AVAV", "AeroVironment Inc.", 285.60m), + ("ATGE", "Allegiant Travel Company", 95.20m), + ("ALK", "Alaska Air Group Inc.", 42.60m), + ("DAL", "Delta Air Lines Inc.", 52.40m), + ("UAL", "United Airlines Holdings Inc.", 68.20m), + + // Additional Energy & Resources + ("RDS.A", "Royal Dutch Shell PLC Class A", 58.40m), + ("TTE", "TotalEnergies SE", 65.80m), + ("ENB", "Enbridge Inc.", 38.20m), + ("TC", "TC Energy Corporation", 52.40m), + ("PAA", "Plains All American Pipeline L.P.", 28.60m), + ("WMB", "Williams Companies Inc.", 42.40m), + ("NGL", "NGL Energy Partners L.P.", 22.80m), + ("ARCH", "Arch Coal Inc.", 148.60m), + ("BTU", "Peabody Energy Corporation", 28.40m), + ("AR", "Antero Resources Corporation", 32.20m), + + // Additional Real Estate & Property + ("SKT", "Tanger Inc.", 18.40m), + ("WPG", "Washington Prime Group Inc.", 2.80m), + ("KIM", "Kimco Realty Corporation", 25.20m), + ("REG", "Regency Centers Corporation", 68.40m), + ("RHP", "Retail Opportunity Investments Corp.", 38.60m), + ("SITM", "Sitemark Holdings Inc.", 15.40m), + ("STOR", "STORE Capital Corporation", 38.20m), + ("MAIN", "Mainstay Apartment Communities Inc.", 12.80m), + ("NHI", "National Health Investors Inc.", 52.40m), + ("LTC", "LTC Properties Inc.", 28.60m), + + // Additional Utilities & Energy Infrastructure + ("PSA", "Public Storage", 385.20m), + ("EQR", "Equity Residential", 68.40m), + ("AVB", "AvalonBay Communities Inc.", 242.60m), + ("CPT", "Camden Property Trust", 95.20m), + ("UMH", "UMH Properties Inc.", 22.60m), + ("AGR", "Agrify Holdings Inc.", 2.40m), + ("VATE", "Viata Energy Inc.", 8.20m), + ("PRIM", "Primotech Inc.", 5.60m), + ("NWLI", "National Western Life Group Inc.", 385.80m), + ("TXRH", "Texas Roadhouse Inc.", 78.20m), + + // Additional Diversified / Conglomerate + ("ITW", "Illinois Tool Works Inc.", 245.80m), + ("HOG", "Harley-Davidson Inc.", 35.20m), + ("LEG", "Leggett & Platt Incorporated", 28.40m), + ("WHR", "Whirlpool Corporation", 125.60m), + ("NWL", "Newell Brands Inc.", 18.40m), + ("SCCO", "Southern Copper Corporation", 95.20m), + ("TECK", "Teck Resources Limited", 28.60m), + ("VALE", "Vale S.A.", 12.40m), + ("RIO", "Rio Tinto Limited", 68.20m), + ("BHP", "BHP Group Limited", 52.80m), + }; + + if (_initialized) + return; + + int id = 1; + foreach (var (symbol, company, price) in stockData) + { + var stock = new Stock + { + StockId = id++, + Symbol = symbol, + Company = company, + CurrentPrice = price, + PreviousPrice = price, + Change = 0, + ChangePercent = 0, + Volume = _random.Next(1000000, 100000000), + LastUpdated = DateTime.Now + }; + + // Initialize display values + stock.UpdateDisplayValues(); + + Stocks.Add(stock); + } + + _initialized = true; + } + + /// + /// Get all stocks from the static collection + /// + public static List GetAllStocks() + { + if (!_initialized) + { + InitializeStocks(); + } + return Stocks; + } + + /// + /// Update all display values based on current raw values + /// Called after every price update + /// + public void UpdateDisplayValues() + { + CurrentPriceDisplay = FormatPrice(CurrentPrice); + ChangeDisplay = FormatCurrency(Change); + ChangePercentDisplay = FormatPercent(ChangePercent); + VolumeDisplay = FormatVolume(Volume); + } + + /// + /// Format decimal as USD currency with directional arrow + /// + private static string FormatCurrency(decimal amount) + { + if (amount < 0) + { + return $"▼ {amount.ToString("C2")}"; + } + else if (amount > 0) + { + return $"▲ {amount.ToString("C2")}"; + } + return amount.ToString("C2"); + } + + /// + /// Format current price as USD currency without directional arrow + /// + private static string FormatPrice(decimal amount) + { + return amount.ToString("C2"); + } + + /// + /// Format decimal as percentage with + or - sign and directional arrow + /// + private static string FormatPercent(decimal value) + { + if (value < 0) + { + return $" {value:F2}%"; + } + else if (value > 0) + { + return $" +{value:F2}%"; + } + return $"• {value:F2}%"; // Neutral indicator + } + + /// + /// Format long number as abbreviated volume (1.2M, 500K, etc.) + /// + private static string FormatVolume(long volume) + { + if (volume >= 1000000) + { + return $"{(volume / 1000000.0):F1}M"; + } + else if (volume >= 1000) + { + return $"{(volume / 1000.0):F1}K"; + } + return volume.ToString(); + } + } +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Program.cs b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Program.cs new file mode 100644 index 0000000..25c4adb --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Program.cs @@ -0,0 +1,47 @@ +using SignalR.Server.Hubs; +using SignalR.Server.Services; +using Newtonsoft.Json.Serialization; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSignalR(); // Add SignalR services +builder.Services.AddScoped(); // Add Stock Data Service +builder.Services.AddHostedService(); // Add Stock Update Service +// Add services to the container. +builder.Services.AddControllers() + .AddNewtonsoftJson(options => + { + // Use the default contract resolver so property names are not camel-cased + options.SerializerSettings.ContractResolver = new DefaultContractResolver(); + }); +// Add services to the container. + +builder.Services.AddCors(options => +{ + options.AddPolicy("CORSPolicy", + builder => builder + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials() + .SetIsOriginAllowed((hosts) => true)); +}); +builder.Services.AddControllersWithViews(); +var app = builder.Build(); +// Configure the HTTP request pipeline. +app.UseCors("CORSPolicy"); // CORS must be before UseHttpsRedirection +if (!app.Environment.IsDevelopment()) +{ + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + app.UseHttpsRedirection(); // Only redirect HTTPS in production +} +app.UseStaticFiles(); +app.UseRouting(); +app.MapHub("/stockHub"); // Map the StockHub - MUST be after UseRouting +app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + +app.MapFallbackToFile("index.html"); + +app.Run(); diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Properties/launchSettings.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Properties/launchSettings.json new file mode 100644 index 0000000..5519ac4 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Properties/launchSettings.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:3573", + "sslPort": 44347 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5083", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + //"launchUrl": "swagger", + "applicationUrl": "https://localhost:7011;http://localhost:5083", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + } + } + } +} + diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Services/StockDataService.cs b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Services/StockDataService.cs new file mode 100644 index 0000000..93ffccb --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Services/StockDataService.cs @@ -0,0 +1,12 @@ +using SignalR.Server.Models; + +namespace SignalR.Server.Services +{ + public class StockDataService + { + public List GetAllStocks() + { + return Stock.GetAllStocks(); + } + } +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Services/StockUpdateService.cs b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Services/StockUpdateService.cs new file mode 100644 index 0000000..944ad79 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Services/StockUpdateService.cs @@ -0,0 +1,85 @@ +using Microsoft.AspNetCore.SignalR; +using SignalR.Server.Hubs; +using SignalR.Server.Models; + +namespace SignalR.Server.Services +{ + /// + /// Background service to simulate and broadcast stock price updates + /// + public class StockUpdateService : BackgroundService + { + private readonly IHubContext _hubContext; + private readonly ILogger _logger; + private readonly Random _random = new Random(); + + public StockUpdateService(IHubContext hubContext, ILogger logger) + { + _hubContext = hubContext; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Stock Update Service is starting."); + + // Initialize stocks + Stock.GetAllStocks(); + + try + { + while (!stoppingToken.IsCancellationRequested) + { + // Update stock prices + UpdateStockPrices(); + + // Broadcast updated stocks to all clients in the StockTraders group + await _hubContext.Clients.Group("StockTraders").SendAsync("ReceiveStockUpdate", Stock.Stocks); + + _logger.LogInformation($"Stock prices updated at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}"); + + // Wait for 2 seconds before next update + await Task.Delay(2000, stoppingToken); + } + } + catch (OperationCanceledException) + { + _logger.LogInformation("Stock Update Service is stopping."); + } + catch (Exception ex) + { + _logger.LogError($"Error in Stock Update Service: {ex.Message}"); + } + } + + /// + /// Update stock prices with random fluctuations + /// + private void UpdateStockPrices() + { + foreach (var stock in Stock.Stocks) + { + stock.PreviousPrice = stock.CurrentPrice; + + // Generate random price change between -2% and +2% + decimal changePercent = (decimal)(_random.NextDouble() * 4 - 2); // -2 to +2 + decimal priceChange = stock.CurrentPrice * (changePercent / 100); + stock.CurrentPrice = Math.Max(stock.CurrentPrice + priceChange, 0.01m); // Ensure price stays positive + + // Calculate change and change percent + stock.Change = stock.CurrentPrice - stock.PreviousPrice; + stock.ChangePercent = stock.PreviousPrice > 0 ? (stock.Change / stock.PreviousPrice) * 100 : 0; + + // Update volume randomly + stock.Volume = stock.Volume + (long)(_random.Next(-5000000, 5000000)); + stock.Volume = Math.Max(stock.Volume, 1000000); // Ensure volume is positive + + // Update timestamp + stock.LastUpdated = DateTime.UtcNow; + + // Update display values for UI rendering + stock.UpdateDisplayValues(); + } + } + } +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.csproj b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.csproj new file mode 100644 index 0000000..4190ab5 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + ..\signalr.client + npm start + https://localhost:58982 + + + + + 8.*-* + + + + + + + + + + + false + + + + diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.csproj.user b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.csproj.user new file mode 100644 index 0000000..9ff5820 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.csproj.user @@ -0,0 +1,6 @@ + + + + https + + \ No newline at end of file diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.http b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.http new file mode 100644 index 0000000..1c8bf1d --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.http @@ -0,0 +1,6 @@ +@SignalR.Server_HostAddress = http://localhost:5083 + +GET {{SignalR.Server_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/WeatherForecast.cs b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/WeatherForecast.cs new file mode 100644 index 0000000..9e122d9 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace SignalR.Server +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/appsettings.Development.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/appsettings.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.sln b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.sln new file mode 100644 index 0000000..07f2040 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.sln @@ -0,0 +1,33 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35919.96 d17.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{54A90642-561A-4BB1-A94E-469ADEE60C69}") = "signalr.client", "signalr.client\signalr.client.esproj", "{1B3BDA74-44F3-1C73-EFE8-8B2935647C2C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignalR.Server", "SignalR.Server\SignalR.Server.csproj", "{C2EDC42D-F025-4867-B892-F4877F473A21}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1B3BDA74-44F3-1C73-EFE8-8B2935647C2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B3BDA74-44F3-1C73-EFE8-8B2935647C2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B3BDA74-44F3-1C73-EFE8-8B2935647C2C}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {1B3BDA74-44F3-1C73-EFE8-8B2935647C2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B3BDA74-44F3-1C73-EFE8-8B2935647C2C}.Release|Any CPU.Build.0 = Release|Any CPU + {1B3BDA74-44F3-1C73-EFE8-8B2935647C2C}.Release|Any CPU.Deploy.0 = Release|Any CPU + {C2EDC42D-F025-4867-B892-F4877F473A21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2EDC42D-F025-4867-B892-F4877F473A21}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2EDC42D-F025-4867-B892-F4877F473A21}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2EDC42D-F025-4867-B892-F4877F473A21}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {868C1DB1-C5AA-4019-8703-971C218262EE} + EndGlobalSection +EndGlobal diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.editorconfig b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.editorconfig new file mode 100644 index 0000000..59d9a3a --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.gitignore b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.gitignore new file mode 100644 index 0000000..0711527 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/angular.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/angular.json new file mode 100644 index 0000000..e59cf43 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/angular.json @@ -0,0 +1,113 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "signalr.client": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "standalone": false + }, + "@schematics/angular:directive": { + "standalone": false + }, + "@schematics/angular:pipe": { + "standalone": false + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/signalr.client", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "signalr.client:build:production" + }, + "development": { + "buildTarget": "signalr.client:build:development" + } + }, + "defaultConfiguration": "development", + "options": { + "proxyConfig": "src/proxy.conf.js", + "port": 58982 + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "signalr.client:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.css" + ], + "scripts": [], + "karmaConfig": "karma.conf.js" + } + } + } + } + }, + "cli": { + "analytics": "4d29b86f-c300-4222-8f61-3b55bbdf27a3" + } +} \ No newline at end of file diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/aspnetcore-https.js b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/aspnetcore-https.js new file mode 100644 index 0000000..cab03ec --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/aspnetcore-https.js @@ -0,0 +1,37 @@ +// This script sets up HTTPS for the application using the ASP.NET Core HTTPS certificate +const fs = require('fs'); +const spawn = require('child_process').spawn; +const path = require('path'); + +const baseFolder = + process.env.APPDATA !== undefined && process.env.APPDATA !== '' + ? `${process.env.APPDATA}/ASP.NET/https` + : `${process.env.HOME}/.aspnet/https`; + +const certificateArg = process.argv.map(arg => arg.match(/--name=(?.+)/i)).filter(Boolean)[0]; +const certificateName = certificateArg ? certificateArg.groups.value : process.env.npm_package_name; + +if (!certificateName) { + console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<> explicitly.') + process.exit(-1); +} + +const certFilePath = path.join(baseFolder, `${certificateName}.pem`); +const keyFilePath = path.join(baseFolder, `${certificateName}.key`); + +if (!fs.existsSync(baseFolder)) { + fs.mkdirSync(baseFolder, { recursive: true }); +} + +if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { + spawn('dotnet', [ + 'dev-certs', + 'https', + '--export-path', + certFilePath, + '--format', + 'Pem', + '--no-password', + ], { stdio: 'inherit', }) + .on('exit', (code) => process.exit(code)); +} \ No newline at end of file diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/karma.conf.js b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/karma.conf.js new file mode 100644 index 0000000..efe6efe --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/karma.conf.js @@ -0,0 +1,44 @@ +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true, + listenAddress: 'localhost', + hostname: 'localhost' + }); +}; + diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/package.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/package.json new file mode 100644 index 0000000..8eb8173 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/package.json @@ -0,0 +1,47 @@ +{ + "name": "signalr.client", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "run-script-os", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "prestart": "node aspnetcore-https", + "start:windows": "ng serve --ssl --ssl-cert \"%APPDATA%\\ASP.NET\\https\\%npm_package_name%.pem\" --ssl-key \"%APPDATA%\\ASP.NET\\https\\%npm_package_name%.key\" --host=127.0.0.1 --proxy-config proxy.conf.json", + "start:default": "ng serve --ssl --ssl-cert \"$HOME/.aspnet/https/${npm_package_name}.pem\" --ssl-key \"$HOME/.aspnet/https/${npm_package_name}.key\" --host=127.0.0.1 --proxy-config proxy.conf.json" + }, + "private": true, + "dependencies": { + "-": "^0.0.1", + "@angular/animations": "^17.0.0", + "@angular/common": "^17.0.0", + "@angular/compiler": "^17.0.0", + "@angular/core": "^17.0.0", + "@angular/forms": "^17.0.0", + "@angular/platform-browser": "^17.0.0", + "@angular/platform-browser-dynamic": "^17.0.0", + "@angular/router": "^17.0.0", + "@microsoft/signalr": "^8.0.7", + "@syncfusion/ej2-angular-grids": "*", + "jest-editor-support": "*", + "rxjs": "~7.8.0", + "save": "^2.9.0", + "tslib": "^2.3.0", + "zone.js": "~0.14.2" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^17.0.3", + "@angular/cli": "^17.0.3", + "@angular/compiler-cli": "^17.0.0", + "@types/jasmine": "~5.1.0", + "jasmine-core": "~5.1.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "run-script-os": "^1.1.6", + "typescript": "~5.2.2" + } +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/proxy.conf.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/proxy.conf.json new file mode 100644 index 0000000..0a09553 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/proxy.conf.json @@ -0,0 +1,13 @@ +{ + "/hubs": { + "target": "http://localhost:5083", + "secure": false, + "changeOrigin": true, + "ws": true + }, + "/api": { + "target": "http://localhost:5083", + "secure": false, + "changeOrigin": true + } +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/signalr.client.esproj b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/signalr.client.esproj new file mode 100644 index 0000000..6631ad9 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/signalr.client.esproj @@ -0,0 +1,10 @@ + + + npm start + Jasmine + + false + + $(MSBuildProjectDirectory)\dist\signalr.client\browser\ + + \ No newline at end of file diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.css b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.css new file mode 100644 index 0000000..8578d16 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.css @@ -0,0 +1,222 @@ +/* Stock Grid Styles */ + +.app-container { + padding: 20px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.stock-header { + margin-bottom: 20px; +} + +.stock-header h1 { + margin: 0; + font-size: 28px; + color: #1a1a1a; + font-weight: 600; +} + +.stock-header .subtitle { + margin: 8px 0 0 0; + font-size: 14px; + color: #666; + font-weight: 400; +} + +/* Stock Grid Customization */ + +/* Symbol Cell */ +.symbol-pill { + display: inline-block; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 6px 12px; + border-radius: 20px; + font-weight: 600; + font-size: 13px; + letter-spacing: 0.5px; +} + +/* symbol templates */ + +.symbol-text { + display: inline-block; + background-color: #eef6ff; /* pale blue background */ + color: #2563eb; /* blue text */ + border: 1px solid #c7e0ff; + padding: 4px 10px; + border-radius: 999px; + font-weight: 700; + font-size: 12px; + box-shadow: 0 1px 0 rgba(37,99,235,0.05); +} + +.symbol-text:hover { + background-color: #e6f0ff; + transform: translateY(-1px); +} + +/* Company Name */ +.company-name { + font-size: 13px; + color: #444; + font-weight: 500; +} + +.company-cell { + display: flex; + align-items: center; +} + +/* Grid cell styles using Syncfusion classes */ + +/* Positive/Green cells */ +.e-grid .e-rowcell.e-poscell { + color: #10b981; + font-weight: 700; + background-color: #ecfdf5; + padding: 8px 12px; + border-radius: 4px; + font-size: 14px; +} + +.e-grid tbody .e-row .e-rowcell.e-poscell { + color: #10b981; + font-weight: 700; + background-color: #ecfdf5; + padding: 8px 12px; + border-radius: 4px; + font-size: 14px; +} + +/* Negative/Red cells */ +.e-grid .e-rowcell.e-negcell { + color: #ef4444; + font-weight: 700; + background-color: #fef2f2; + padding: 8px 12px; + border-radius: 4px; + font-size: 14px; +} + +.e-grid tbody .e-row .e-rowcell.e-negcell { + color: #ef4444; + font-weight: 700; + background-color: #fef2f2; + padding: 8px 12px; + border-radius: 4px; + font-size: 14px; +} + +/* Price column: blue text, no background */ +.e-grid .e-rowcell.e-price { + color: #2563eb; + font-weight: 700; + background-color: transparent; + padding: 8px 12px; + font-size: 14px; +} + +.e-grid tbody .e-row .e-rowcell.e-price { + color: #2563eb; + font-weight: 700; + background-color: transparent; + padding: 8px 12px; + font-size: 14px; +} + +.e-grid .e-rowcell.e-volumecell { + + font-weight: 600; + font-size: 14px; + padding: 8px 12px; + border-radius: 6px; + text-align: right; +} + +.e-grid tbody .e-row .e-rowcell.e-volumecell { + color: #11543e; + font-weight: 600; + font-size: 14px; + padding: 8px 12px; + border-radius: 6px; + text-align: right; +} + +/* Timestamp Cell */ +.timestamp { + color: #999; + font-size: 12px; +} + + +/* Up/Down Symbols */ +.symbol { + font-weight: 700; + margin-right: 6px; + font-size: 14px; + display: inline-block; +} + +.value { + font-weight: 700; + font-size: 14px; +} + +/* Change cell with styling */ +.change-cell { + display: flex; + align-items: center; + gap: 4px; +} + +.change-cell .symbol { + margin: 0; +} + +/* Change percent cell */ +.change-percent-cell { + display: flex; + align-items: center; + gap: 4px; +} + +.change-percent-cell .symbol { + margin: 0; +} + + + +/* Responsive adjustments */ +@media (max-width: 768px) { + .app-container { + padding: 10px; + } + + .stock-header h1 { + font-size: 20px; + } + + .stock-header .subtitle { + font-size: 12px; + } + + .symbol-pill { + font-size: 11px; + padding: 4px 8px; + } + + .company-name { + font-size: 11px; + } + + .price { + font-size: 12px; + } + + .change-percent { + font-size: 12px; + padding: 2px 6px; + } +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.html b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.html new file mode 100644 index 0000000..69cce1b --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.html @@ -0,0 +1,30 @@ +
+
+

Live Stock Market

+

Real-time stock prices updated every 2 seconds via SignalR

+
+ + + + + +
+ {{data.Symbol}} +
+
+
+ + +
+
{{data.Company}}
+
+
+
+ + + + + +
+
+
\ No newline at end of file diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.spec.ts b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.spec.ts new file mode 100644 index 0000000..64dc497 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.spec.ts @@ -0,0 +1,45 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + let component: AppComponent; + let fixture: ComponentFixture; + let httpMock: HttpTestingController; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AppComponent], + imports: [HttpClientTestingModule] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AppComponent); + component = fixture.componentInstance; + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should create the app', () => { + expect(component).toBeTruthy(); + }); + + it('should retrieve weather forecasts from the server', () => { + const mockForecasts = [ + { date: '2021-10-01', temperatureC: 20, temperatureF: 68, summary: 'Mild' }, + { date: '2021-10-02', temperatureC: 25, temperatureF: 77, summary: 'Warm' } + ]; + + component.ngOnInit(); + + const req = httpMock.expectOne('/weatherforecast'); + expect(req.request.method).toEqual('GET'); + req.flush(mockForecasts); + + expect(component.forecasts).toEqual(mockForecasts); + }); +}); \ No newline at end of file diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.ts b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.ts new file mode 100644 index 0000000..2c63bdd --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.ts @@ -0,0 +1,157 @@ +import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core'; +import { GridComponent, ToolbarItems, FilterService, SortService, ToolbarService, GridModule, FilterSettingsModel } from '@syncfusion/ej2-angular-grids'; +import { DataManager, UrlAdaptor } from '@syncfusion/ej2-data'; +import { EditSettingsModel } from '@syncfusion/ej2-angular-grids'; +import { HubConnection } from '@microsoft/signalr'; +import * as signalR from '@microsoft/signalr'; +import { HttpClient } from '@angular/common/http'; + +// client model matching server-side Stock model (PascalCase) +export interface StockItem { + stockId: number; + symbol: string; + company: string; + currentPrice: number; + previousPrice?: number; + change: number; + changePercent: number; + changeDisplay?: string; + changePercentDisplay?: string; + volume: number; + volumeDisplay?: string; + lastUpdated?: string; +} + +@Component({ + selector: 'app-root', + standalone:true, + providers:[FilterService, SortService, ToolbarService], + templateUrl: 'app.component.html', + styleUrls: ['app.component.css'], + imports:[GridModule] +}) +export class AppComponent implements OnInit, OnDestroy { + @ViewChild('grid') + public grid?: GridComponent; + public data?: DataManager; + public editSettings?: EditSettingsModel; + public toolbar?: ToolbarItems[]; + private connection!: HubConnection; + private localData: StockItem[] = []; + public filterSettings! : FilterSettingsModel; + + constructor(private http: HttpClient) {} + + ngOnInit(): void { + this. filterSettings = { type:'Excel'} + // Use the Stock UrlDatasource endpoint (Syncfusion expects POST) + this.data = new DataManager({ + url: 'http://localhost:5083/api/Stock/UrlDatasource', + adaptor: new UrlAdaptor(), + // headers are optional; kept for parity with the React sample + headers: [ + { 'Content-Type': 'application/json' } + ] + }); + + // Enable editing if needed + this.editSettings = { allowEditing: true, allowAdding: true, allowDeleting: true, mode: 'Normal' }; + this.toolbar = ['Search']; + + // build hub connection to the StockHub (server maps to /stockHub) + this.connection = new signalR.HubConnectionBuilder() + .withUrl('http://localhost:5083/stockHub', { + skipNegotiation: false, + transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.LongPolling + }) + .withAutomaticReconnect() + .build(); + } + + created() { + // Initial snapshot from hub + this.connection.on('InitializeStocks', (items: StockItem[]) => { + if (!items || items.length === 0) return; + if (!this.grid) return; + const grid = this.grid as any; + console.log("created") + // Update existing rows by primary key using setCellValue to avoid full refresh + items.forEach((stock) => { + try { + grid?.setCellValue(stock.stockId, 'CurrentPrice', stock.currentPrice); + grid?.setCellValue(stock.stockId, 'ChangeDisplay', stock.changeDisplay); + grid?.setCellValue(stock.stockId, 'ChangePercentDisplay', stock.changePercentDisplay); + grid?.setCellValue(stock.stockId, 'VolumeDisplay', stock.volumeDisplay); + grid?.setCellValue(stock.stockId, 'LastUpdated', stock.lastUpdated); + } catch (err) { + console.error(`Error updating stock ${stock?.symbol}:`, err); + } + }); + }); + + // Real-time updates + this.connection.on('ReceiveStockUpdate', (updates: StockItem[]) => { + if (!updates || updates.length === 0) return; + if (!this.grid) return; + const grid = this.grid as any; + updates.forEach((stock) => { + grid?.setCellValue(stock.stockId, 'CurrentPrice', stock.currentPrice); + grid?.setCellValue(stock.stockId, 'ChangeDisplay', stock.changeDisplay); + grid?.setCellValue(stock.stockId, 'ChangePercentDisplay', stock.changePercentDisplay); + grid?.setCellValue(stock.stockId, 'VolumeDisplay', stock.volumeDisplay); + grid?.setCellValue(stock.stockId, 'LastUpdated', stock.lastUpdated); + }); + }); + + // Start connection and subscribe to group + this.connection.start() + .then(() => { + console.log('SignalR connected to /stockHub'); + this.connection.invoke('SubscribeToStocks') + .catch(err => console.error('SubscribeToStocks failed', err)); + }) + .catch(err => console.error('SignalR start failed', err)); + } + + // Optional: style cells similar to React queryCellInfo + queryCellInfo(args: any) { + if (args.column?.field === 'CurrentPrice') { + args.cell?.classList.add('e-price'); + } else if (args.column?.field === 'ChangeDisplay') { + const change = args.data?.Change ?? 0; + if (change < 0) args.cell?.classList.add('e-negcell'); + else if (change > 0) args.cell?.classList.add('e-poscell'); + } else if (args.column?.field === 'ChangePercentDisplay') { + const cp = args.data?.ChangePercent ?? 0; + if (cp < 0) args.cell?.classList.add('e-negcell'); + else if (cp > 0) args.cell?.classList.add('e-poscell'); + } else if (args.column?.field === 'VolumeDisplay') { + args.cell?.classList.add('e-volumecell'); + } else if (args.column?.field === 'LastUpdated') { + const raw = args.data?.LastUpdated; + if (raw) { + const dt = new Date(raw); + const day = String(dt.getDate()).padStart(2, '0'); + const mon = dt.toLocaleString('en-US', { month: 'short' }); + const year = dt.getFullYear(); + let hour = dt.getHours(); + hour = hour % 12 || 12; + const hh = String(hour).padStart(2, '0'); + const mm = String(dt.getMinutes()).padStart(2, '0'); + const ss = String(dt.getSeconds()).padStart(2, '0'); + const formatted = `${day} ${mon} ${year} ${hh}:${mm}:${ss}`; + if (args.cell) args.cell.textContent = formatted; + } + } + } + + ngOnDestroy(): void { + if (this.connection) { + try { + this.connection.invoke('UnsubscribeFromStocks').catch(() => {}); + this.connection.stop(); + } catch { } + } + } + +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/assets/.gitkeep b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/favicon.ico b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 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 Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/index.html b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/index.html new file mode 100644 index 0000000..c0a6cc9 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/index.html @@ -0,0 +1,14 @@ + + + + + SignalrClient + + + + + + + + + diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/main.ts b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/main.ts new file mode 100644 index 0000000..d7496c2 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/main.ts @@ -0,0 +1,13 @@ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { bootstrapApplication } from '@angular/platform-browser'; +import { provideHttpClient } from '@angular/common/http'; +import { provideAnimations } from '@angular/platform-browser/animations'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, { + providers: [ + provideHttpClient(), + provideAnimations() + ] +}).catch(err => console.error(err)); diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/proxy.conf.js b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/proxy.conf.js new file mode 100644 index 0000000..6edf1c3 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/proxy.conf.js @@ -0,0 +1,16 @@ +const { env } = require('process'); + +const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` : + env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'https://localhost:7011'; + +const PROXY_CONFIG = [ + { + context: [ + "/weatherforecast", + ], + target, + secure: false + } +] + +module.exports = PROXY_CONFIG; diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/styles.css b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/styles.css new file mode 100644 index 0000000..c46a6f8 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/styles.css @@ -0,0 +1,9 @@ +@import '../node_modules/@syncfusion/ej2-base/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-buttons/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-calendars/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-dropdowns/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-inputs/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-navigations/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-popups/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-splitbuttons/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-angular-grids/styles/tailwind.css'; diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.app.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.app.json new file mode 100644 index 0000000..374cc9d --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.app.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.json new file mode 100644 index 0000000..678336b --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.json @@ -0,0 +1,32 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": [ + "ES2022", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.spec.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.spec.json new file mode 100644 index 0000000..be7e9da --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.spec.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} From 1eb0f14a7faff72ba05eab750fa4d01b35a0d367 Mon Sep 17 00:00:00 2001 From: Sivaranjani Rajasekaran Date: Fri, 10 Apr 2026 13:19:40 +0530 Subject: [PATCH 2/6] 1014281 - Added signalR sample --- .../ReadMe.md | 57 +++-- .../Controllers/StockController.cs | 2 +- .../SignalR.Server/Models/Stock.cs | 2 +- .../SignalR.Server/Program.cs | 19 +- .../SignalR.Server/SignalR.Server.csproj | 6 +- .../signalr.client/.editorconfig | 16 -- .../signalr.client/.gitignore | 42 ---- .../signalr.client/angular.json | 113 --------- .../signalr.client/aspnetcore-https.js | 37 --- .../signalr.client/karma.conf.js | 44 ---- .../signalr.client/package.json | 68 +++--- .../signalr.client/proxy.conf.json | 13 - .../signalr.client/src/app/app.component.css | 222 ------------------ .../signalr.client/src/app/app.component.html | 30 --- .../src/app/app.component.spec.ts | 45 ---- .../signalr.client/src/app/app.component.ts | 157 ------------- .../signalr.client/src/assets/.gitkeep | 0 .../signalr.client/src/favicon.ico | Bin 15086 -> 0 bytes .../signalr.client/src/index.html | 14 -- .../signalr.client/src/main.ts | 13 - .../signalr.client/src/proxy.conf.js | 16 -- .../signalr.client/src/styles.css | 9 - .../signalr.client/tsconfig.app.json | 34 ++- .../signalr.client/tsconfig.json | 35 +-- .../signalr.client/tsconfig.spec.json | 14 -- 25 files changed, 107 insertions(+), 901 deletions(-) delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.editorconfig delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.gitignore delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/angular.json delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/aspnetcore-https.js delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/karma.conf.js delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/proxy.conf.json delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.css delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.html delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.spec.ts delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.ts delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/assets/.gitkeep delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/favicon.ico delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/index.html delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/main.ts delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/proxy.conf.js delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/styles.css delete mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.spec.json diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/ReadMe.md b/connecting-to-backends/syncfusion-reactgrid-with-signalr/ReadMe.md index 7585ad9..d6ec111 100644 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/ReadMe.md +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/ReadMe.md @@ -1,16 +1,20 @@ -# Syncfusion Angular Grid with SignalR +# Syncfusion React Grid with SignalR -The Syncfusion Angular Grid component supports real-time data binding using SignalR, enabling automatic grid updates as data changes on the server. This capability proves essential for applications requiring live updates and multi-client synchronization. +The Syncfusion® React Grid supports real-time data binding using SignalR, a powerful library for bi-directional communication between servers and clients. This approach enables live data updates without page refreshes, making it ideal for applications that require instant information delivery such as stock tickers, live dashboards, and real-time notifications. -## Key Features +**What is SignalR?** -- **Real-Time Communication**: Establish persistent connections for instant data updates across all connected clients. -- **Bidirectional**: Support both server-to-client (broadcasting) and client-to-server (commands) communication. -- **Automatic Transport Selection**: Intelligently choose the best transport protocol (WebSockets, SSE, Long Polling) based on browser and server capabilities. -- **Scalable Broadcasting**: Efficiently broadcast updates to multiple clients simultaneously using SignalR groups. -- **Built-in Reconnection**: Automatically handles client reconnection with exponential back off retry logic. -- **No Page Refresh Required**: Update UI dynamically without reloading the page. -Cross-Platform: Works across browsers, mobile devices, and desktop applications. +[SignalR](https://learn.microsoft.com/en-us/aspnet/signalr/) is an open-source .NET library that simplifies adding real-time web functionality to applications. It automatically handles the best transport method (WebSockets, Server-Sent Events, or Long Polling) and provides a high-level API for server-to-client and client-to-server communication. SignalR enables persistent two-way connections between clients and servers, allowing instant data synchronization without polling. + +**Key benefits of SignalR** + +- **Real-time communication**: Establish persistent connections for instant data updates across all connected clients. +- **Bidirectional**: Support both server-to-client (broadcasting) and client-to-server communication. +- **Automatic transport selection**: Intelligently choose the best transport protocol (WebSockets, SSE, Long Polling) based on browser and server capabilities. +- **Scalable broadcasting**: Efficiently broadcast updates to multiple clients simultaneously using SignalR groups. +- **Built-in reconnection**: Automatically handles client reconnection with exponential back off retry logic. +- **No page refresh required**: Update UI dynamically without reloading the page. +- **Cross-platform**: Works across browsers, mobile devices, and desktop applications. ## Prerequisites @@ -18,8 +22,8 @@ Cross-Platform: Works across browsers, mobile devices, and desktop applications. | **Software / Package** | **Recommended version** | **Purpose** | |-----------------------------|------------------------------|-------------------------------------- | | Node.js | 20.x LTS or later | Runtime | -| npm / yarn / pnpm | 9.x or later | Package manager | -| Angular CLI | 17.x | Build and serve the Angular client | +| npm / yarn / pnpm | 11.x or later | Package manager | +| Vite | 7.3.1 | Use this to create the React application | | TypeScript | 5.x or later | Server‑side and client‑side type safety | ## Quick Start @@ -51,24 +55,27 @@ Cross-Platform: Works across browsers, mobile devices, and desktop applications. npm install npm run dev ``` -- Open **https://127.0.0.1:58982/** in the browser. +- Open **http://localhost:5173/** in the browser. ## Project Layout | **File/Folder** | **Purpose** | |-------------|---------| -| `signalr.client/package.json` | Client package manifest and dev/start scripts (`npm start`, `npm run dev`). | -| `signalr.client/src/main.ts` | Angular standalone bootstrap (bootstraps `AppComponent` with providers). | -| `signalr.client/src/app/app.component.ts` | Client grid logic (DataManager, SignalR hub connection, `setCellValue` updates). | -| `signalr.client/src/app/app.component.html` | Grid markup and column definitions for the Syncfusion grid. | -| `signalr.client/src/app/app.component.css` | Client UI styles and cell styling classes. | -| `SignalR.Server/` | ASP.NET Core backend with SignalR hubs, APIs, and background update service. | -| `SignalR.Server/Program.cs` | Server startup: DI, SignalR, controllers, CORS, and hub mappings (`/stockHub`). | -| `SignalR.Server/Controllers/StockController.cs` | Syncfusion `UrlDatasource` endpoint and other REST endpoints (GetAll, GetById, statistics). | -| `SignalR.Server/Hubs/StockHub.cs` | SignalR hub that sends `InitializeStocks` and manages subscriptions. | -| `SignalR.Server/Models/Stock.cs` | Server `Stock` model including raw values and `*Display` formatted fields. | -| `SignalR.Server/Services/StockUpdateService.cs` | Background service that simulates price updates and broadcasts them to the `StockTraders` group. | +| `signalr.client/package.json` | Client package manifest and dev/start scripts | +| `signalr.client/tsconfig.json` / `tsconfig.app.json` | TypeScript configuration files for the client | +| `signalr.client/src/main.tsx` | React application entry point | +| `signalr.client/src/index.css` | Global styles for the client app | +| `signalr.client/src/components/StockGrid.tsx` | React component that renders the Syncfusion Grid and uses SignalR for live updates | +| `signalr.client/src/styles/StockGrid.css` | Styles for the `StockGrid` component (chips, colors, layout) | +| `SignalR.Server/Program.cs` | Server entry configuring services, middleware, and SignalR hubs (maps `/stockHub`) | +| `SignalR.Server/Controllers/StockController.cs` | API endpoints for initial grid datasource and CRUD operations | +| `SignalR.Server/Hubs/StockHub.cs` | Lightweight SignalR hub; injects `StockDataService`, manages group membership (`StockTraders`) and sends initial `InitializeStocks` | +| `SignalR.Server/Models/Stock.cs` | Server-side `Stock` model with raw fields and `*Display` formatted fields | +| `SignalR.Server/Services/StockUpdateService.cs` | Background service that simulates price updates and broadcasts updates to the `StockTraders` group | +| `SignalR.Server/Services/StockDataService.cs` | Small service wrapper around `Stock.GetAllStocks()` used by `StockHub` | +| `SignalR.Server/appsettings.json` / `appsettings.Development.json` | Server configuration files | +| `SignalR.Server/SignalR.Server.csproj` | Server project file with dependencies and build settings | ## Common Tasks @@ -87,7 +94,7 @@ Cross-Platform: Works across browsers, mobile devices, and desktop applications. 2. **Copy the GitHub URL** - Navigate to the sample folder you want to download and copy its URL. - - Example : https://github.com/SyncfusionExamples/ej2-angular-grid-samples/tree/master/connecting-to-backends/syncfusion-angular-grid-apollo-server + - Example : https://github.com/SyncfusionExamples/ej2-react-grid-samples/tree/master/connecting-to-backends/syncfusion-reactgrid-with-django-server 3. **Paste the URL into DownGit** diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/StockController.cs b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/StockController.cs index f8a46f7..d17131d 100644 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/StockController.cs +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/StockController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using SignalR.Server.Models; diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Models/Stock.cs b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Models/Stock.cs index 86ff7fc..4face6e 100644 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Models/Stock.cs +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Models/Stock.cs @@ -90,7 +90,7 @@ static Stock() } private static void InitializeStocks() - { + { var stockData = new[] { // Technology diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Program.cs b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Program.cs index 25c4adb..0d5fee9 100644 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Program.cs +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Program.cs @@ -1,6 +1,5 @@ using SignalR.Server.Hubs; using SignalR.Server.Services; -using Newtonsoft.Json.Serialization; var builder = WebApplication.CreateBuilder(args); @@ -9,10 +8,9 @@ builder.Services.AddHostedService(); // Add Stock Update Service // Add services to the container. builder.Services.AddControllers() - .AddNewtonsoftJson(options => + .AddJsonOptions(options => { - // Use the default contract resolver so property names are not camel-cased - options.SerializerSettings.ContractResolver = new DefaultContractResolver(); + options.JsonSerializerOptions.PropertyNamingPolicy = null; // Use PascalCase }); // Add services to the container. @@ -23,20 +21,27 @@ .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials() + .WithOrigins("http://localhost:5173", "http://localhost:5174", "http://localhost:3000") // Explicitly allow React dev server .SetIsOriginAllowed((hosts) => true)); }); builder.Services.AddControllersWithViews(); var app = builder.Build(); + // Configure the HTTP request pipeline. -app.UseCors("CORSPolicy"); // CORS must be before UseHttpsRedirection -if (!app.Environment.IsDevelopment()) +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} +else { // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); - app.UseHttpsRedirection(); // Only redirect HTTPS in production + app.UseHttpsRedirection(); } + app.UseStaticFiles(); app.UseRouting(); +app.UseCors("CORSPolicy"); // CORS must be after UseRouting but before MapHub app.MapHub("/stockHub"); // Map the StockHub - MUST be after UseRouting app.MapControllerRoute( name: "default", diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.csproj b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.csproj index 4190ab5..848489b 100644 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.csproj +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/SignalR.Server.csproj @@ -15,15 +15,13 @@ - + + - - false - diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.editorconfig b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.editorconfig deleted file mode 100644 index 59d9a3a..0000000 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.editorconfig +++ /dev/null @@ -1,16 +0,0 @@ -# Editor configuration, see https://editorconfig.org -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -insert_final_newline = true -trim_trailing_whitespace = true - -[*.ts] -quote_type = single - -[*.md] -max_line_length = off -trim_trailing_whitespace = false diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.gitignore b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.gitignore deleted file mode 100644 index 0711527..0000000 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/.gitignore +++ /dev/null @@ -1,42 +0,0 @@ -# See http://help.github.com/ignore-files/ for more about ignoring files. - -# Compiled output -/dist -/tmp -/out-tsc -/bazel-out - -# Node -/node_modules -npm-debug.log -yarn-error.log - -# IDEs and editors -.idea/ -.project -.classpath -.c9/ -*.launch -.settings/ -*.sublime-workspace - -# Visual Studio Code -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -.history/* - -# Miscellaneous -/.angular/cache -.sass-cache/ -/connect.lock -/coverage -/libpeerconnection.log -testem.log -/typings - -# System files -.DS_Store -Thumbs.db diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/angular.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/angular.json deleted file mode 100644 index e59cf43..0000000 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/angular.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "version": 1, - "newProjectRoot": "projects", - "projects": { - "signalr.client": { - "projectType": "application", - "schematics": { - "@schematics/angular:component": { - "standalone": false - }, - "@schematics/angular:directive": { - "standalone": false - }, - "@schematics/angular:pipe": { - "standalone": false - } - }, - "root": "", - "sourceRoot": "src", - "prefix": "app", - "architect": { - "build": { - "builder": "@angular-devkit/build-angular:application", - "options": { - "outputPath": "dist/signalr.client", - "index": "src/index.html", - "browser": "src/main.ts", - "polyfills": [ - "zone.js" - ], - "tsConfig": "tsconfig.app.json", - "assets": [ - "src/favicon.ico", - "src/assets" - ], - "styles": [ - "src/styles.css" - ], - "scripts": [] - }, - "configurations": { - "production": { - "budgets": [ - { - "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" - }, - { - "type": "anyComponentStyle", - "maximumWarning": "2kb", - "maximumError": "4kb" - } - ], - "outputHashing": "all" - }, - "development": { - "optimization": false, - "extractLicenses": false, - "sourceMap": true - } - }, - "defaultConfiguration": "production" - }, - "serve": { - "builder": "@angular-devkit/build-angular:dev-server", - "configurations": { - "production": { - "buildTarget": "signalr.client:build:production" - }, - "development": { - "buildTarget": "signalr.client:build:development" - } - }, - "defaultConfiguration": "development", - "options": { - "proxyConfig": "src/proxy.conf.js", - "port": 58982 - } - }, - "extract-i18n": { - "builder": "@angular-devkit/build-angular:extract-i18n", - "options": { - "buildTarget": "signalr.client:build" - } - }, - "test": { - "builder": "@angular-devkit/build-angular:karma", - "options": { - "polyfills": [ - "zone.js", - "zone.js/testing" - ], - "tsConfig": "tsconfig.spec.json", - "assets": [ - "src/favicon.ico", - "src/assets" - ], - "styles": [ - "src/styles.css" - ], - "scripts": [], - "karmaConfig": "karma.conf.js" - } - } - } - } - }, - "cli": { - "analytics": "4d29b86f-c300-4222-8f61-3b55bbdf27a3" - } -} \ No newline at end of file diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/aspnetcore-https.js b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/aspnetcore-https.js deleted file mode 100644 index cab03ec..0000000 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/aspnetcore-https.js +++ /dev/null @@ -1,37 +0,0 @@ -// This script sets up HTTPS for the application using the ASP.NET Core HTTPS certificate -const fs = require('fs'); -const spawn = require('child_process').spawn; -const path = require('path'); - -const baseFolder = - process.env.APPDATA !== undefined && process.env.APPDATA !== '' - ? `${process.env.APPDATA}/ASP.NET/https` - : `${process.env.HOME}/.aspnet/https`; - -const certificateArg = process.argv.map(arg => arg.match(/--name=(?.+)/i)).filter(Boolean)[0]; -const certificateName = certificateArg ? certificateArg.groups.value : process.env.npm_package_name; - -if (!certificateName) { - console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<> explicitly.') - process.exit(-1); -} - -const certFilePath = path.join(baseFolder, `${certificateName}.pem`); -const keyFilePath = path.join(baseFolder, `${certificateName}.key`); - -if (!fs.existsSync(baseFolder)) { - fs.mkdirSync(baseFolder, { recursive: true }); -} - -if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { - spawn('dotnet', [ - 'dev-certs', - 'https', - '--export-path', - certFilePath, - '--format', - 'Pem', - '--no-password', - ], { stdio: 'inherit', }) - .on('exit', (code) => process.exit(code)); -} \ No newline at end of file diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/karma.conf.js b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/karma.conf.js deleted file mode 100644 index efe6efe..0000000 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/karma.conf.js +++ /dev/null @@ -1,44 +0,0 @@ -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage'), - require('@angular-devkit/build-angular/plugins/karma') - ], - client: { - jasmine: { - // you can add configuration options for Jasmine here - // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html - // for example, you can disable the random execution with `random: false` - // or set a specific seed with `seed: 4321` - }, - clearContext: false // leave Jasmine Spec Runner output visible in browser - }, - jasmineHtmlReporter: { - suppressAll: true // removes the duplicated traces - }, - coverageReporter: { - dir: require('path').join(__dirname, './coverage/'), - subdir: '.', - reporters: [ - { type: 'html' }, - { type: 'text-summary' } - ] - }, - reporters: ['progress', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true, - listenAddress: 'localhost', - hostname: 'localhost' - }); -}; - diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/package.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/package.json index 8eb8173..c3749c5 100644 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/package.json +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/package.json @@ -1,47 +1,39 @@ { - "name": "signalr.client", + "name": "client", + "private": true, "version": "0.0.0", + "type": "module", "scripts": { - "ng": "ng", - "start": "run-script-os", - "build": "ng build", - "watch": "ng build --watch --configuration development", - "test": "ng test", - "prestart": "node aspnetcore-https", - "start:windows": "ng serve --ssl --ssl-cert \"%APPDATA%\\ASP.NET\\https\\%npm_package_name%.pem\" --ssl-key \"%APPDATA%\\ASP.NET\\https\\%npm_package_name%.key\" --host=127.0.0.1 --proxy-config proxy.conf.json", - "start:default": "ng serve --ssl --ssl-cert \"$HOME/.aspnet/https/${npm_package_name}.pem\" --ssl-key \"$HOME/.aspnet/https/${npm_package_name}.key\" --host=127.0.0.1 --proxy-config proxy.conf.json" + "start": "vite", + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" }, - "private": true, "dependencies": { - "-": "^0.0.1", - "@angular/animations": "^17.0.0", - "@angular/common": "^17.0.0", - "@angular/compiler": "^17.0.0", - "@angular/core": "^17.0.0", - "@angular/forms": "^17.0.0", - "@angular/platform-browser": "^17.0.0", - "@angular/platform-browser-dynamic": "^17.0.0", - "@angular/router": "^17.0.0", - "@microsoft/signalr": "^8.0.7", - "@syncfusion/ej2-angular-grids": "*", - "jest-editor-support": "*", - "rxjs": "~7.8.0", - "save": "^2.9.0", - "tslib": "^2.3.0", - "zone.js": "~0.14.2" + "@microsoft/signalr": "^10.0.0", + "@syncfusion/ej2-base": "*", + "@syncfusion/ej2-data": "*", + "@syncfusion/ej2-react-buttons": "*", + "@syncfusion/ej2-react-calendars": "*", + "@syncfusion/ej2-react-dropdowns": "*", + "@syncfusion/ej2-react-grids": "*", + "@syncfusion/ej2-react-inputs": "*", + "react": "^19.2.0", + "react-dom": "^19.2.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^17.0.3", - "@angular/cli": "^17.0.3", - "@angular/compiler-cli": "^17.0.0", - "@types/jasmine": "~5.1.0", - "jasmine-core": "~5.1.0", - "karma": "~6.4.0", - "karma-chrome-launcher": "~3.2.0", - "karma-coverage": "~2.2.0", - "karma-jasmine": "~5.1.0", - "karma-jasmine-html-reporter": "~2.1.0", - "run-script-os": "^1.1.6", - "typescript": "~5.2.2" + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.8", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" } } diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/proxy.conf.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/proxy.conf.json deleted file mode 100644 index 0a09553..0000000 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/proxy.conf.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "/hubs": { - "target": "http://localhost:5083", - "secure": false, - "changeOrigin": true, - "ws": true - }, - "/api": { - "target": "http://localhost:5083", - "secure": false, - "changeOrigin": true - } -} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.css b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.css deleted file mode 100644 index 8578d16..0000000 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.css +++ /dev/null @@ -1,222 +0,0 @@ -/* Stock Grid Styles */ - -.app-container { - padding: 20px; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.stock-header { - margin-bottom: 20px; -} - -.stock-header h1 { - margin: 0; - font-size: 28px; - color: #1a1a1a; - font-weight: 600; -} - -.stock-header .subtitle { - margin: 8px 0 0 0; - font-size: 14px; - color: #666; - font-weight: 400; -} - -/* Stock Grid Customization */ - -/* Symbol Cell */ -.symbol-pill { - display: inline-block; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - padding: 6px 12px; - border-radius: 20px; - font-weight: 600; - font-size: 13px; - letter-spacing: 0.5px; -} - -/* symbol templates */ - -.symbol-text { - display: inline-block; - background-color: #eef6ff; /* pale blue background */ - color: #2563eb; /* blue text */ - border: 1px solid #c7e0ff; - padding: 4px 10px; - border-radius: 999px; - font-weight: 700; - font-size: 12px; - box-shadow: 0 1px 0 rgba(37,99,235,0.05); -} - -.symbol-text:hover { - background-color: #e6f0ff; - transform: translateY(-1px); -} - -/* Company Name */ -.company-name { - font-size: 13px; - color: #444; - font-weight: 500; -} - -.company-cell { - display: flex; - align-items: center; -} - -/* Grid cell styles using Syncfusion classes */ - -/* Positive/Green cells */ -.e-grid .e-rowcell.e-poscell { - color: #10b981; - font-weight: 700; - background-color: #ecfdf5; - padding: 8px 12px; - border-radius: 4px; - font-size: 14px; -} - -.e-grid tbody .e-row .e-rowcell.e-poscell { - color: #10b981; - font-weight: 700; - background-color: #ecfdf5; - padding: 8px 12px; - border-radius: 4px; - font-size: 14px; -} - -/* Negative/Red cells */ -.e-grid .e-rowcell.e-negcell { - color: #ef4444; - font-weight: 700; - background-color: #fef2f2; - padding: 8px 12px; - border-radius: 4px; - font-size: 14px; -} - -.e-grid tbody .e-row .e-rowcell.e-negcell { - color: #ef4444; - font-weight: 700; - background-color: #fef2f2; - padding: 8px 12px; - border-radius: 4px; - font-size: 14px; -} - -/* Price column: blue text, no background */ -.e-grid .e-rowcell.e-price { - color: #2563eb; - font-weight: 700; - background-color: transparent; - padding: 8px 12px; - font-size: 14px; -} - -.e-grid tbody .e-row .e-rowcell.e-price { - color: #2563eb; - font-weight: 700; - background-color: transparent; - padding: 8px 12px; - font-size: 14px; -} - -.e-grid .e-rowcell.e-volumecell { - - font-weight: 600; - font-size: 14px; - padding: 8px 12px; - border-radius: 6px; - text-align: right; -} - -.e-grid tbody .e-row .e-rowcell.e-volumecell { - color: #11543e; - font-weight: 600; - font-size: 14px; - padding: 8px 12px; - border-radius: 6px; - text-align: right; -} - -/* Timestamp Cell */ -.timestamp { - color: #999; - font-size: 12px; -} - - -/* Up/Down Symbols */ -.symbol { - font-weight: 700; - margin-right: 6px; - font-size: 14px; - display: inline-block; -} - -.value { - font-weight: 700; - font-size: 14px; -} - -/* Change cell with styling */ -.change-cell { - display: flex; - align-items: center; - gap: 4px; -} - -.change-cell .symbol { - margin: 0; -} - -/* Change percent cell */ -.change-percent-cell { - display: flex; - align-items: center; - gap: 4px; -} - -.change-percent-cell .symbol { - margin: 0; -} - - - -/* Responsive adjustments */ -@media (max-width: 768px) { - .app-container { - padding: 10px; - } - - .stock-header h1 { - font-size: 20px; - } - - .stock-header .subtitle { - font-size: 12px; - } - - .symbol-pill { - font-size: 11px; - padding: 4px 8px; - } - - .company-name { - font-size: 11px; - } - - .price { - font-size: 12px; - } - - .change-percent { - font-size: 12px; - padding: 2px 6px; - } -} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.html b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.html deleted file mode 100644 index 69cce1b..0000000 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.html +++ /dev/null @@ -1,30 +0,0 @@ -
-
-

Live Stock Market

-

Real-time stock prices updated every 2 seconds via SignalR

-
- - - - - -
- {{data.Symbol}} -
-
-
- - -
-
{{data.Company}}
-
-
-
- - - - - -
-
-
\ No newline at end of file diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.spec.ts b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.spec.ts deleted file mode 100644 index 64dc497..0000000 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { AppComponent } from './app.component'; - -describe('AppComponent', () => { - let component: AppComponent; - let fixture: ComponentFixture; - let httpMock: HttpTestingController; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [AppComponent], - imports: [HttpClientTestingModule] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(AppComponent); - component = fixture.componentInstance; - httpMock = TestBed.inject(HttpTestingController); - }); - - afterEach(() => { - httpMock.verify(); - }); - - it('should create the app', () => { - expect(component).toBeTruthy(); - }); - - it('should retrieve weather forecasts from the server', () => { - const mockForecasts = [ - { date: '2021-10-01', temperatureC: 20, temperatureF: 68, summary: 'Mild' }, - { date: '2021-10-02', temperatureC: 25, temperatureF: 77, summary: 'Warm' } - ]; - - component.ngOnInit(); - - const req = httpMock.expectOne('/weatherforecast'); - expect(req.request.method).toEqual('GET'); - req.flush(mockForecasts); - - expect(component.forecasts).toEqual(mockForecasts); - }); -}); \ No newline at end of file diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.ts b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.ts deleted file mode 100644 index 2c63bdd..0000000 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/app/app.component.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core'; -import { GridComponent, ToolbarItems, FilterService, SortService, ToolbarService, GridModule, FilterSettingsModel } from '@syncfusion/ej2-angular-grids'; -import { DataManager, UrlAdaptor } from '@syncfusion/ej2-data'; -import { EditSettingsModel } from '@syncfusion/ej2-angular-grids'; -import { HubConnection } from '@microsoft/signalr'; -import * as signalR from '@microsoft/signalr'; -import { HttpClient } from '@angular/common/http'; - -// client model matching server-side Stock model (PascalCase) -export interface StockItem { - stockId: number; - symbol: string; - company: string; - currentPrice: number; - previousPrice?: number; - change: number; - changePercent: number; - changeDisplay?: string; - changePercentDisplay?: string; - volume: number; - volumeDisplay?: string; - lastUpdated?: string; -} - -@Component({ - selector: 'app-root', - standalone:true, - providers:[FilterService, SortService, ToolbarService], - templateUrl: 'app.component.html', - styleUrls: ['app.component.css'], - imports:[GridModule] -}) -export class AppComponent implements OnInit, OnDestroy { - @ViewChild('grid') - public grid?: GridComponent; - public data?: DataManager; - public editSettings?: EditSettingsModel; - public toolbar?: ToolbarItems[]; - private connection!: HubConnection; - private localData: StockItem[] = []; - public filterSettings! : FilterSettingsModel; - - constructor(private http: HttpClient) {} - - ngOnInit(): void { - this. filterSettings = { type:'Excel'} - // Use the Stock UrlDatasource endpoint (Syncfusion expects POST) - this.data = new DataManager({ - url: 'http://localhost:5083/api/Stock/UrlDatasource', - adaptor: new UrlAdaptor(), - // headers are optional; kept for parity with the React sample - headers: [ - { 'Content-Type': 'application/json' } - ] - }); - - // Enable editing if needed - this.editSettings = { allowEditing: true, allowAdding: true, allowDeleting: true, mode: 'Normal' }; - this.toolbar = ['Search']; - - // build hub connection to the StockHub (server maps to /stockHub) - this.connection = new signalR.HubConnectionBuilder() - .withUrl('http://localhost:5083/stockHub', { - skipNegotiation: false, - transport: signalR.HttpTransportType.WebSockets | signalR.HttpTransportType.LongPolling - }) - .withAutomaticReconnect() - .build(); - } - - created() { - // Initial snapshot from hub - this.connection.on('InitializeStocks', (items: StockItem[]) => { - if (!items || items.length === 0) return; - if (!this.grid) return; - const grid = this.grid as any; - console.log("created") - // Update existing rows by primary key using setCellValue to avoid full refresh - items.forEach((stock) => { - try { - grid?.setCellValue(stock.stockId, 'CurrentPrice', stock.currentPrice); - grid?.setCellValue(stock.stockId, 'ChangeDisplay', stock.changeDisplay); - grid?.setCellValue(stock.stockId, 'ChangePercentDisplay', stock.changePercentDisplay); - grid?.setCellValue(stock.stockId, 'VolumeDisplay', stock.volumeDisplay); - grid?.setCellValue(stock.stockId, 'LastUpdated', stock.lastUpdated); - } catch (err) { - console.error(`Error updating stock ${stock?.symbol}:`, err); - } - }); - }); - - // Real-time updates - this.connection.on('ReceiveStockUpdate', (updates: StockItem[]) => { - if (!updates || updates.length === 0) return; - if (!this.grid) return; - const grid = this.grid as any; - updates.forEach((stock) => { - grid?.setCellValue(stock.stockId, 'CurrentPrice', stock.currentPrice); - grid?.setCellValue(stock.stockId, 'ChangeDisplay', stock.changeDisplay); - grid?.setCellValue(stock.stockId, 'ChangePercentDisplay', stock.changePercentDisplay); - grid?.setCellValue(stock.stockId, 'VolumeDisplay', stock.volumeDisplay); - grid?.setCellValue(stock.stockId, 'LastUpdated', stock.lastUpdated); - }); - }); - - // Start connection and subscribe to group - this.connection.start() - .then(() => { - console.log('SignalR connected to /stockHub'); - this.connection.invoke('SubscribeToStocks') - .catch(err => console.error('SubscribeToStocks failed', err)); - }) - .catch(err => console.error('SignalR start failed', err)); - } - - // Optional: style cells similar to React queryCellInfo - queryCellInfo(args: any) { - if (args.column?.field === 'CurrentPrice') { - args.cell?.classList.add('e-price'); - } else if (args.column?.field === 'ChangeDisplay') { - const change = args.data?.Change ?? 0; - if (change < 0) args.cell?.classList.add('e-negcell'); - else if (change > 0) args.cell?.classList.add('e-poscell'); - } else if (args.column?.field === 'ChangePercentDisplay') { - const cp = args.data?.ChangePercent ?? 0; - if (cp < 0) args.cell?.classList.add('e-negcell'); - else if (cp > 0) args.cell?.classList.add('e-poscell'); - } else if (args.column?.field === 'VolumeDisplay') { - args.cell?.classList.add('e-volumecell'); - } else if (args.column?.field === 'LastUpdated') { - const raw = args.data?.LastUpdated; - if (raw) { - const dt = new Date(raw); - const day = String(dt.getDate()).padStart(2, '0'); - const mon = dt.toLocaleString('en-US', { month: 'short' }); - const year = dt.getFullYear(); - let hour = dt.getHours(); - hour = hour % 12 || 12; - const hh = String(hour).padStart(2, '0'); - const mm = String(dt.getMinutes()).padStart(2, '0'); - const ss = String(dt.getSeconds()).padStart(2, '0'); - const formatted = `${day} ${mon} ${year} ${hh}:${mm}:${ss}`; - if (args.cell) args.cell.textContent = formatted; - } - } - } - - ngOnDestroy(): void { - if (this.connection) { - try { - this.connection.invoke('UnsubscribeFromStocks').catch(() => {}); - this.connection.stop(); - } catch { } - } - } - -} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/assets/.gitkeep b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/favicon.ico b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/favicon.ico deleted file mode 100644 index 57614f9c967596fad0a3989bec2b1deff33034f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 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 Nr__JTFzvMr^FKp4S3v*( diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/index.html b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/index.html deleted file mode 100644 index c0a6cc9..0000000 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - SignalrClient - - - - - - - - - diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/main.ts b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/main.ts deleted file mode 100644 index d7496c2..0000000 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/main.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; - -import { bootstrapApplication } from '@angular/platform-browser'; -import { provideHttpClient } from '@angular/common/http'; -import { provideAnimations } from '@angular/platform-browser/animations'; -import { AppComponent } from './app/app.component'; - -bootstrapApplication(AppComponent, { - providers: [ - provideHttpClient(), - provideAnimations() - ] -}).catch(err => console.error(err)); diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/proxy.conf.js b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/proxy.conf.js deleted file mode 100644 index 6edf1c3..0000000 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/proxy.conf.js +++ /dev/null @@ -1,16 +0,0 @@ -const { env } = require('process'); - -const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` : - env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'https://localhost:7011'; - -const PROXY_CONFIG = [ - { - context: [ - "/weatherforecast", - ], - target, - secure: false - } -] - -module.exports = PROXY_CONFIG; diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/styles.css b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/styles.css deleted file mode 100644 index c46a6f8..0000000 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/styles.css +++ /dev/null @@ -1,9 +0,0 @@ -@import '../node_modules/@syncfusion/ej2-base/styles/tailwind.css'; -@import '../node_modules/@syncfusion/ej2-buttons/styles/tailwind.css'; -@import '../node_modules/@syncfusion/ej2-calendars/styles/tailwind.css'; -@import '../node_modules/@syncfusion/ej2-dropdowns/styles/tailwind.css'; -@import '../node_modules/@syncfusion/ej2-inputs/styles/tailwind.css'; -@import '../node_modules/@syncfusion/ej2-navigations/styles/tailwind.css'; -@import '../node_modules/@syncfusion/ej2-popups/styles/tailwind.css'; -@import '../node_modules/@syncfusion/ej2-splitbuttons/styles/tailwind.css'; -@import '../node_modules/@syncfusion/ej2-angular-grids/styles/tailwind.css'; diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.app.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.app.json index 374cc9d..a9b5a59 100644 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.app.json +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.app.json @@ -1,14 +1,28 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ { - "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "./out-tsc/app", - "types": [] + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true }, - "files": [ - "src/main.ts" - ], - "include": [ - "src/**/*.d.ts" - ] + "include": ["src"] } diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.json index 678336b..1ffef60 100644 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.json +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.json @@ -1,32 +1,7 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ { - "compileOnSave": false, - "compilerOptions": { - "outDir": "./dist/out-tsc", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "sourceMap": true, - "declaration": false, - "experimentalDecorators": true, - "moduleResolution": "node", - "importHelpers": true, - "target": "ES2022", - "module": "ES2022", - "useDefineForClassFields": false, - "lib": [ - "ES2022", - "dom" - ] - }, - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true - } + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] } diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.spec.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.spec.json deleted file mode 100644 index be7e9da..0000000 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.spec.json +++ /dev/null @@ -1,14 +0,0 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./out-tsc/spec", - "types": [ - "jasmine" - ] - }, - "include": [ - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] -} From 6b4e2a06c58168e23a449ab667212d86d830f545 Mon Sep 17 00:00:00 2001 From: Sivaranjani Rajasekaran Date: Fri, 10 Apr 2026 15:17:24 +0530 Subject: [PATCH 3/6] update the code --- .../signalr.client/eslint.config.js | 23 ++ .../signalr.client/index.html | 14 + .../signalr.client/public/vite.svg | 1 + .../signalr.client/src/assets/react.svg | 1 + .../src/components/StockGrid.tsx | 339 ++++++++++++++++++ .../signalr.client/src/index.css | 10 + .../signalr.client/src/main.tsx | 7 + .../signalr.client/src/styles/StockGrid.css | 223 ++++++++++++ .../signalr.client/tsconfig.node.json | 26 ++ .../signalr.client/vite.config.ts | 7 + 10 files changed, 651 insertions(+) create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/eslint.config.js create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/index.html create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/public/vite.svg create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/assets/react.svg create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/components/StockGrid.tsx create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/index.css create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/main.tsx create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/styles/StockGrid.css create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.node.json create mode 100644 connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/vite.config.ts diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/eslint.config.js b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/index.html b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/index.html new file mode 100644 index 0000000..c27c586 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/index.html @@ -0,0 +1,14 @@ + + + + + + + client + + + +
+ + + diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/public/vite.svg b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/assets/react.svg b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/components/StockGrid.tsx b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/components/StockGrid.tsx new file mode 100644 index 0000000..a39a4e7 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/components/StockGrid.tsx @@ -0,0 +1,339 @@ +import React, { useRef, useEffect, useMemo } from "react"; +import * as SignalR from '@microsoft/SignalR'; +import { + GridComponent, + ColumnsDirective, + ColumnDirective, + Inject, + Page, + Sort, + Filter, + Search, + Toolbar, +} from "@syncfusion/ej2-react-grids"; +import "../styles/StockGrid.css"; +import { DataManager, UrlAdaptor } from "@syncfusion/ej2-data"; + +const StockGrid: React.FC = () => { + const gridRef = useRef(null); + const connectionRef = useRef(null); + + const stock = useMemo(() => { + return new DataManager({ + url: 'http://localhost:5083/api/Stock/UrlDatasource', + adaptor: new UrlAdaptor(), + crossDomain: true, + }); + }, []); + + // Initialize SignalR connection for real-time updates + useEffect(() => { + let isSubscribed = true; // Track if component is still mounted + let isSubscriptionPending = false; // Prevent duplicate subscription calls + + const conn = new SignalR.HubConnectionBuilder() + .withUrl("http://localhost:5083/stockHub", { + withCredentials: true, + skipNegotiation: false, + transport: SignalR.HttpTransportType.WebSockets | SignalR.HttpTransportType.LongPolling + }) + .withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) + .configureLogging(SignalR.LogLevel.Information) + .build(); + + connectionRef.current = conn; + + // Handler for initial stock data on connection + conn.on("InitializeStocks", (stocks: any[]) => { + if (!stocks || stocks.length === 0) { + return; + } + + if (!gridRef.current) { + return; + } + + const grid = gridRef.current as any; + + // Update each stock's cells using formatted display values from server + stocks.forEach((stock) => { + try { + grid?.setCellValue(stock.stockId, 'CurrentPrice', stock.currentPrice); + grid?.setCellValue(stock.stockId, 'ChangeDisplay', stock.changeDisplay); + grid?.setCellValue(stock.stockId, 'ChangePercentDisplay', stock.changePercentDisplay); + grid?.setCellValue(stock.stockId, 'VolumeDisplay', stock.volumeDisplay); + grid?.setCellValue(stock.stockId, 'LastUpdated', stock.lastUpdated); + } catch (err) { + console.error(`Error updating stock ${stock?.symbol}:`, err); + } + }); + }); + + // Handler for real-time stock updates + conn.on("ReceiveStockUpdate", (updatedStocks: any[]) => { + if (!updatedStocks || updatedStocks.length === 0) { + return; + } + + if (!gridRef.current) { + return; + } + + const grid = gridRef.current as any; + + // Update each stock's cells with real-time data + updatedStocks.forEach((stock) => { + try { + grid?.setCellValue(stock.stockId, 'CurrentPrice', stock.currentPrice); + grid?.setCellValue(stock.stockId, 'ChangeDisplay', stock.changeDisplay); + grid?.setCellValue(stock.stockId, 'ChangePercentDisplay', stock.changePercentDisplay); + grid?.setCellValue(stock.stockId, 'VolumeDisplay', stock.volumeDisplay); + grid?.setCellValue(stock.stockId, 'LastUpdated', stock.lastUpdated); + } catch (err) { + console.error(`Error updating stock ${stock?.symbol}:`, err); + } + }); + }); + + // Helper function to subscribe - called only once per connection + const subscribeToStocks = async () => { + if (isSubscriptionPending || !isSubscribed) { + return; + } + + isSubscriptionPending = true; + try { + await conn.invoke("SubscribeToStocks"); + if (isSubscribed) { + console.log("✅ Subscribed to stock updates"); + } + } catch (err) { + if (isSubscribed) { + console.error("❌ Error subscribing to stocks:", err); + } + } finally { + isSubscriptionPending = false; + } + }; + + // Connection state logging + conn.onreconnecting((error) => { + console.log(`SignalR connection lost. Attempting to reconnect: ${error?.message}`); + }); + + conn.onclose((error) => { + console.error(`SignalR connection closed: ${error?.message || "Unknown error"}`); + }); + + // Start connection - do not manually call subscribe here + conn.start() + .then(() => { + if (!isSubscribed) { + // Component unmounted during connection - stop immediately + conn.stop(); + return; + } + console.log("✅ SignalR connection established successfully"); + return subscribeToStocks(); + }) + .catch((err: Error) => { + if (isSubscribed) { + console.error("❌ Error establishing SignalR connection:", err.message); + } + }); + + return () => { + isSubscribed = false; // Mark as unsubscribed + + if (connectionRef.current) { + const currentConn = connectionRef.current; + + // Only try to unsubscribe if connection is in Connected state + if (currentConn.state === SignalR.HubConnectionState.Connected) { + currentConn.invoke("UnsubscribeFromStocks") + .catch(() => { + // Silently catch - connection might already be closing + }); + } + + currentConn.off("InitializeStocks"); + currentConn.off("ReceiveStockUpdate"); + + currentConn.stop().catch(() => { + // Silently catch - already stopped or stopping + }); + + connectionRef.current = null; + } + }; + }, []); + + const filterSettings = { type: "Excel" as const }; + const toolbar: string[] = ["Search"]; + + // Column templates for custom styling + const symbolTemplate = (props: any) => ( +
+
{props?.Symbol}
+
+ ); + + const companyTemplate = (props: any) => ( +
+
{props?.Company}
+
+ ); + + // Helper function to determine if a value is positive or negative by parsing the display string + const isPositiveChange = (displayValue: string): boolean => { + if (!displayValue) return false; + // Check for positive indicators: starts with "+" or contains "▲" (up arrow) + return displayValue.includes('+') || displayValue.includes('▲'); + }; + + const isNegativeChange = (displayValue: string): boolean => { + if (!displayValue) return false; + // Check for negative indicators: starts with "-" or contains "▼" (down arrow) or "(" for parentheses notation + return displayValue.includes('-') || displayValue.includes('▼') || displayValue.includes('('); + }; + + // queryCellInfo handler - applies CSS classes for styling based on column type and display values + const queryCellInfo = (args: any) => { + try { + // Remove all possible styling classes first + args.cell?.classList.remove('e-poscell', 'e-negcell', 'e-volumecell', 'e-price'); + + const columnField = args.column?.field; + + // CURRENT PRICE: Blue text, no background + if (columnField === 'CurrentPrice') { + args.cell?.classList.add('e-price'); + } + + // CHANGE DISPLAY: Parse the display string to determine color + else if (columnField === 'ChangeDisplay') { + const changeDisplay = args.data?.ChangeDisplay ?? ''; + if (isNegativeChange(changeDisplay)) { + args.cell?.classList.add('e-negcell'); // RED for price drop + } else if (isPositiveChange(changeDisplay)) { + args.cell?.classList.add('e-poscell'); // GREEN for price increase + } + } + + // CHANGE PERCENT DISPLAY: Parse the display string to determine color + else if (columnField === 'ChangePercentDisplay') { + const changePercentDisplay = args.data?.ChangePercentDisplay ?? ''; + if (isNegativeChange(changePercentDisplay)) { + args.cell?.classList.add('e-negcell'); // RED for percentage drop + } else if (isPositiveChange(changePercentDisplay)) { + args.cell?.classList.add('e-poscell'); // GREEN for percentage increase + } + } + + // VOLUME: Default styling + else if (columnField === 'VolumeDisplay') { + args.cell?.classList.add('e-volumecell'); + } + + // LAST UPDATED: Format the timestamp + else if (columnField === 'LastUpdated') { + const raw = args.data?.LastUpdated; + if (raw) { + const dt = new Date(raw); + const day = String(dt.getDate()).padStart(2, '0'); + const mon = dt.toLocaleString('en-US', { month: 'short' }); + const year = dt.getFullYear(); + let hour = dt.getHours(); + hour = hour % 12 || 12; + const hh = String(hour).padStart(2, '0'); + const mm = String(dt.getMinutes()).padStart(2, '0'); + const ss = String(dt.getSeconds()).padStart(2, '0'); + const formatted = `${day} ${mon} ${year} ${hh}:${mm}:${ss}`; + if (args.cell) args.cell.textContent = formatted; + } + } + } catch (err) { + console.error("Error in queryCellInfo:", err); + } + }; + + return ( +
+
+

Live Stock Market

+

Real-time stock prices updated every 2 seconds via SignalR

+
+ + + + + + + + + + + + + +
+ ); +}; + +export default StockGrid; diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/index.css b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/index.css new file mode 100644 index 0000000..f5428b7 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/index.css @@ -0,0 +1,10 @@ +@import '../node_modules/@syncfusion/ej2-base/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-buttons/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-calendars/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-dropdowns/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-inputs/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-navigations/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-popups/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-splitbuttons/styles/tailwind.css'; +@import '../node_modules/@syncfusion/ej2-notifications/styles/tailwind.css'; +@import "../node_modules/@syncfusion/ej2-react-grids/styles/tailwind.css"; \ No newline at end of file diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/main.tsx b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/main.tsx new file mode 100644 index 0000000..3445c85 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/main.tsx @@ -0,0 +1,7 @@ +import { createRoot } from 'react-dom/client'; +import './index.css'; +import StockGrid from './components/StockGrid.tsx'; + +createRoot(document.getElementById('root')!).render( + +) diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/styles/StockGrid.css b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/styles/StockGrid.css new file mode 100644 index 0000000..1569af7 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/styles/StockGrid.css @@ -0,0 +1,223 @@ +/* Stock Grid Styles */ + +.app-container { + padding: 20px; + margin-top:30px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.stock-header { + margin-bottom: 20px; +} + +.stock-header h1 { + margin: 0; + font-size: 28px; + color: #1a1a1a; + font-weight: 600; +} + +.stock-header .subtitle { + margin: 8px 0 0 0; + font-size: 14px; + color: #666; + font-weight: 400; +} + +/* Stock Grid Customization */ + +/* Symbol Cell */ +.symbol-pill { + display: inline-block; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 6px 12px; + border-radius: 20px; + font-weight: 600; + font-size: 13px; + letter-spacing: 0.5px; +} + +/* symbol templates */ + +.symbol-text { + display: inline-block; + background-color: #eef6ff; /* pale blue background */ + color: #2563eb; /* blue text */ + border: 1px solid #c7e0ff; + padding: 4px 10px; + border-radius: 999px; + font-weight: 700; + font-size: 12px; + box-shadow: 0 1px 0 rgba(37,99,235,0.05); +} + +.symbol-text:hover { + background-color: #e6f0ff; + transform: translateY(-1px); +} + +/* Company Name */ +.company-name { + font-size: 13px; + color: #444; + font-weight: 500; +} + +.company-cell { + display: flex; + align-items: center; +} + +/* Grid cell styles using Syncfusion classes */ + +/* Positive/Green cells */ +.e-grid .e-rowcell.e-poscell { + color: #10b981; + font-weight: 700; + background-color: #ecfdf5; + padding: 8px 12px; + border-radius: 4px; + font-size: 14px; +} + +.e-grid tbody .e-row .e-rowcell.e-poscell { + color: #10b981; + font-weight: 700; + background-color: #ecfdf5; + padding: 8px 12px; + border-radius: 4px; + font-size: 14px; +} + +/* Negative/Red cells */ +.e-grid .e-rowcell.e-negcell { + color: #ef4444; + font-weight: 700; + background-color: #fef2f2; + padding: 8px 12px; + border-radius: 4px; + font-size: 14px; +} + +.e-grid tbody .e-row .e-rowcell.e-negcell { + color: #ef4444; + font-weight: 700; + background-color: #fef2f2; + padding: 8px 12px; + border-radius: 4px; + font-size: 14px; +} + +/* Price column: blue text, no background */ +.e-grid .e-rowcell.e-price { + color: #2563eb; + font-weight: 700; + background-color: transparent; + padding: 8px 12px; + font-size: 14px; +} + +.e-grid tbody .e-row .e-rowcell.e-price { + color: #2563eb; + font-weight: 700; + background-color: transparent; + padding: 8px 12px; + font-size: 14px; +} + +.e-grid .e-rowcell.e-volumecell { + + font-weight: 600; + font-size: 14px; + padding: 8px 12px; + border-radius: 6px; + text-align: right; +} + +.e-grid tbody .e-row .e-rowcell.e-volumecell { + color: #11543e; + font-weight: 600; + font-size: 14px; + padding: 8px 12px; + border-radius: 6px; + text-align: right; +} + +/* Timestamp Cell */ +.timestamp { + color: #999; + font-size: 12px; +} + + +/* Up/Down Symbols */ +.symbol { + font-weight: 700; + margin-right: 6px; + font-size: 14px; + display: inline-block; +} + +.value { + font-weight: 700; + font-size: 14px; +} + +/* Change cell with styling */ +.change-cell { + display: flex; + align-items: center; + gap: 4px; +} + +.change-cell .symbol { + margin: 0; +} + +/* Change percent cell */ +.change-percent-cell { + display: flex; + align-items: center; + gap: 4px; +} + +.change-percent-cell .symbol { + margin: 0; +} + + + +/* Responsive adjustments */ +@media (max-width: 768px) { + .app-container { + padding: 10px; + } + + .stock-header h1 { + font-size: 20px; + } + + .stock-header .subtitle { + font-size: 12px; + } + + .symbol-pill { + font-size: 11px; + padding: 4px 8px; + } + + .company-name { + font-size: 11px; + } + + .price { + font-size: 12px; + } + + .change-percent { + font-size: 12px; + padding: 2px 6px; + } +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.node.json b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/vite.config.ts b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From 1f61fdb7ba541e0090779e09809fdd6a761dc23a Mon Sep 17 00:00:00 2001 From: Ranjani4311 <156400505+Ranjani4311@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:19:29 +0530 Subject: [PATCH 4/6] remove unwanted lines --- .../Controllers/StockController.cs | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/StockController.cs b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/StockController.cs index d17131d..05b07a2 100644 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/StockController.cs +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/SignalR.Server/Controllers/StockController.cs @@ -14,7 +14,7 @@ public class StockController : ControllerBase { /// /// Fetch stock data with support for Syncfusion DataManager operations - /// Supports: Search, Sorting, Filtering, Paging + /// Supports: Search, Sorting, Filtering /// [HttpPost("UrlDatasource")] public IActionResult UrlDatasource([FromBody] DataManagerRequest dm) @@ -168,22 +168,6 @@ public IActionResult GetStatistics() } } - /// - /// CRUD Model for handling Syncfusion DataManager CRUD operations - /// - public class CRUDModel where T : class - { - public string? action { get; set; } - public string? table { get; set; } - public string? keyColumn { get; set; } - public object? key { get; set; } - public T? value { get; set; } - public List? added { get; set; } - public List? changed { get; set; } - public List? deleted { get; set; } - public IDictionary? @params { get; set; } - } - /// /// Where clause model for filtering operations /// From 19cb4cdaa8d175da96a9b8bdb2b4519fe19d6566 Mon Sep 17 00:00:00 2001 From: Ranjani4311 <156400505+Ranjani4311@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:20:49 +0530 Subject: [PATCH 5/6] remove unwanted lines --- .../signalr.client/src/components/StockGrid.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/components/StockGrid.tsx b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/components/StockGrid.tsx index a39a4e7..2f27d9a 100644 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/components/StockGrid.tsx +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/signalr.client/src/components/StockGrid.tsx @@ -5,7 +5,6 @@ import { ColumnsDirective, ColumnDirective, Inject, - Page, Sort, Filter, Search, @@ -330,7 +329,7 @@ const StockGrid: React.FC = () => { textAlign="Center" /> - +
); From 36169dfa44e2bacee7d881cd5e5b0b898c50ae07 Mon Sep 17 00:00:00 2001 From: Ranjani4311 <156400505+Ranjani4311@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:21:45 +0530 Subject: [PATCH 6/6] Clarify API endpoints in ReadMe for StockController Updated the ReadMe file to clarify API endpoints for the StockController. --- .../syncfusion-reactgrid-with-signalr/ReadMe.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/connecting-to-backends/syncfusion-reactgrid-with-signalr/ReadMe.md b/connecting-to-backends/syncfusion-reactgrid-with-signalr/ReadMe.md index d6ec111..229d437 100644 --- a/connecting-to-backends/syncfusion-reactgrid-with-signalr/ReadMe.md +++ b/connecting-to-backends/syncfusion-reactgrid-with-signalr/ReadMe.md @@ -69,7 +69,7 @@ The Syncfusion® React Grid supports real-ti | `signalr.client/src/components/StockGrid.tsx` | React component that renders the Syncfusion Grid and uses SignalR for live updates | | `signalr.client/src/styles/StockGrid.css` | Styles for the `StockGrid` component (chips, colors, layout) | | `SignalR.Server/Program.cs` | Server entry configuring services, middleware, and SignalR hubs (maps `/stockHub`) | -| `SignalR.Server/Controllers/StockController.cs` | API endpoints for initial grid datasource and CRUD operations | +| `SignalR.Server/Controllers/StockController.cs` | API endpoints for initial grid datasource | | `SignalR.Server/Hubs/StockHub.cs` | Lightweight SignalR hub; injects `StockDataService`, manages group membership (`StockTraders`) and sends initial `InitializeStocks` | | `SignalR.Server/Models/Stock.cs` | Server-side `Stock` model with raw fields and `*Display` formatted fields | | `SignalR.Server/Services/StockUpdateService.cs` | Background service that simulates price updates and broadcasts updates to the `StockTraders` group | @@ -103,4 +103,4 @@ The Syncfusion® React Grid supports real-ti 4. **Download the ZIP** - Click **Download**. - - DownGit will generate a ZIP file of the selected folder, which you can save and extract locally. \ No newline at end of file + - DownGit will generate a ZIP file of the selected folder, which you can save and extract locally.