Tích hợp Blazor vào Ứng dụng WinForms với Dữ liệu Thời gian Thực

Để tối giản hóa việc truyền dữ liệu giữa môi trường WinForms và giao diện Blazor, một cách tiếp cận hiệu quả là đăng ký các điều khiển WinForms (như Button hoặc Timer) trực tiếp vào hệ thống DI (Dependency Injection) của .NET. Điều này cho phép các thành phần Razor truy cập trực tiếp vào các điều khiển và sự kiện của chúng mà không cần lớp trung gian. Dưới đây là ví dụ minh họa cách sử dụng Button và Timer từ WinForms trong một component Blazor để hiển thị dữ liệu động lên biểu đồ đường (line chart).

Cấu hình trang HTML nền (index.html)

Trang HTML cung cấp giao diện web nhúng trong BlazorWebView, bao gồm các thư viện như Bootstrap và Chart.js để xử lý đồ họa.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WinFormsBlazor</title>
    <base href="/" />
    <link href="css/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="WinFormsBlazor.styles.css" rel="stylesheet" />
</head>
<body>
    <div id="app" class="container">Đang tải...</div>
    <div id="blazor-error-ui">
        Đã xảy ra lỗi. <a href="" class="reload">Tải lại</a><a class="dismiss">🗙</a>
    </div>

    <script src="_framework/blazor.webview.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.min.js"></script>

    <canvas id="myChart" width="400" height="200"></canvas>

    <script>
        let chartInstance;
        const sampleData = {
            labels: [],
            datasets: [
                {
                    label: 'Người dùng A',
                    data: [],
                    borderColor: '#ff6384',
                    fill: false
                },
                {
                    label: 'Người dùng B',
                    data: [],
                    borderColor: '#36a2eb',
                    fill: false
                }
            ]
        };

        const chartOptions = {
            animation: false,
            scales: {
                y: { min: -300, max: 300 }
            }
        };

        function renderChart() {
            const ctx = document.getElementById('myChart').getContext('2d');
            chartInstance = new Chart(ctx, {
                type: 'line',
                data: sampleData,
                options: chartOptions
            });
        }

        function updateChartData(newData, newLabels) {
            chartInstance.data.labels = newLabels;
            chartInstance.data.datasets[0].data = newData[0];
            chartInstance.data.datasets[1].data = newData[1];
            chartInstance.update();
        }
    </script>
</body>
</html>

Component Razor: DataDisplay.razor

Component này sử dụng @inject để lấy tham chiếu đến Button và Timer đã được đăng ký qua DI. Khi nhấn nút, biểu đồ sẽ khởi tạo và bộ định thời bắt đầu thu thập dữ liệu giả lập.

@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@inject IJSRuntime JS
@inject System.Windows.Forms.Button triggerButton
@inject System.Windows.Forms.Timer pollingTimer

<h3>Biểu đồ theo dõi thời gian thực</h3>
<canvas id="myChart" width="500" height="250"></canvas>

@code {
    private List<int> seriesA = new();
    private List<int> seriesB = new();
    private List<int> timeLabels = new();
    private int timestamp = 0;

    protected override void OnInitialized()
    {
        // Khởi động biểu đồ khi nhấn nút
        triggerButton.Click += async (sender, args) =>
        {
            await JS.InvokeVoidAsync("renderChart");
            pollingTimer.Enabled = true;
        };

        // Cập nhật dữ liệu mỗi lần timer "tick"
        pollingTimer.Tick += async (sender, args) =>
        {
            var rand = new Random();
            seriesA.Add(rand.Next(-100, 100));
            seriesB.Add(rand.Next(-120, 150));
            timeLabels.Add(timestamp++);
            
            // Chỉ giữ lại 20 điểm dữ liệu gần nhất
            if (seriesA.Count > 20)
            {
                seriesA = seriesA.Skip(seriesA.Count - 20).ToList();
                seriesB = seriesB.Skip(seriesB.Count - 20).ToList();
                timeLabels = timeLabels.Skip(timeLabels.Count - 20).ToList();
            }

            await JS.InvokeVoidAsync("updateChartData",
                new[] { seriesA.ToArray(), seriesB.ToArray() },
                timeLabels.ToArray());
        };
    }
}

Form WinForms: MainForm.cs

Form chính khởi tạo BlazorWebView, cấu hình dịch vụ và ánh xạ component Razor vào vùng hiển thị.

using Microsoft.AspNetCore.Components.WebView.WindowsForms;
using Microsoft.Extensions.DependencyInjection;
using System.Windows.Forms;

namespace WinFormsBlazorApp
{
    public partial class MainForm : Form
    {
        private readonly ServiceCollection _services;

        public MainForm()
        {
            InitializeComponent();
            buttonStart.Text = "Bắt đầu thu thập";
            timerPoll.Interval = 1000; // 1 giây
            timerPoll.Enabled = false;

            _services = new ServiceCollection();
            _services.AddWindowsFormsBlazorWebView();
            _services.AddSingleton(buttonStart);  // Inject nút
            _services.AddSingleton(timerPoll);    // Inject timer

            blazorWebView.HostPage = "wwwroot/index.html";
            blazorWebView.Services = _services.BuildServiceProvider();
            blazorWebView.RootComponents.Add<DataDisplay>("#app");
        }
    }
}

Cấu hình dự án (.csproj)

Dự án sử dụng SDK Razor để hỗ trợ Blazor trong ứng dụng desktop.

<Project Sdk="Microsoft.NET.Sdk.Razor">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net7.0-windows</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebView.WindowsForms" Version="6.0.300-rc.1.5355" />
    <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.1222-prerelease" />
  </ItemGroup>

  <ItemGroup>
    <Content Update="DataDisplay.razor">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
  </ItemGroup>
</Project>

Thẻ: blazor WinForms Chart.js IJSInterop DependencyInjection

Đăng vào ngày 14 tháng 10 lúc 17:04