HttpClient
取得 JSON,解析成 StockProfile
清單ObservableCollection<StockProfile>
,即時顯示在 DataGrid為了降低耦合、便於測試,先定義一個介面,由它負責抓 API 並回傳模型清單。
// Models/StockProfile.cs(若已有可沿用)
public class StockProfile
{
public string Code { get; set; }
public string Name { get; set; }
public string Industry { get; set; }
public DateTime LastUpdatedUtc { get; set; }
}
// Services/IStockApiService.cs
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
public interface IStockApiService
{
Task<List<StockProfile>> GetStocksAsync(CancellationToken ct = default);
}
// Services/TwseStockApiService.cs
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
public sealed class TwseStockApiService : IStockApiService
{
private readonly HttpClient _http;
private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
public TwseStockApiService(HttpClient httpClient)
{
_http = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
public async Task<List<StockProfile>> GetStocksAsync(CancellationToken ct = default)
{
// 示意:TWSE 開放資料 (股票代號/名稱)。實務上可能需容錯欄位名。
// 替代:可先用你 Day 12 的 Crawler 包裝好在這裡呼叫。
var url = "https://openapi.twse.com.tw/v1/opendata/t187ap03_L";
string json = await _http.GetStringAsync(url, ct);
// 多數開放資料為陣列物件;這裡示意直接反序列化為動態再投影成模型。
using var doc = JsonDocument.Parse(json);
var result = new List<StockProfile>();
foreach (var e in doc.RootElement.EnumerateArray())
{
string code = e.TryGetProperty("公司代號", out var c1) ? c1.GetString()
: e.TryGetProperty("Code", out var c2) ? c2.GetString()
: null;
string name = e.TryGetProperty("公司簡稱", out var n1) ? n1.GetString()
: e.TryGetProperty("Name", out var n2) ? n2.GetString()
: null;
string industry = e.TryGetProperty("產業別", out var i1) ? i1.GetString()
: e.TryGetProperty("Industry", out var i2) ? i2.GetString()
: null;
if (!string.IsNullOrWhiteSpace(code) && !string.IsNullOrWhiteSpace(name))
{
result.Add(new StockProfile
{
Code = code,
Name = name,
Industry = industry ?? string.Empty,
LastUpdatedUtc = DateTime.UtcNow
});
}
}
return result;
}
}
// Infra/AsyncCommand.cs
using System;
using System.Threading.Tasks;
using System.Windows.Input;
public class AsyncCommand : ICommand
{
private readonly Func<Task> _execute;
private readonly Func<bool> _canExecute;
private bool _isExecuting;
public AsyncCommand(Func<Task> execute, Func<bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => !_isExecuting && (_canExecute?.Invoke() ?? true);
public async void Execute(object parameter)
{
_isExecuting = true; RaiseCanExecuteChanged();
try { await _execute(); }
finally { _isExecuting = false; RaiseCanExecuteChanged(); }
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
ObservableCollection<StockProfile>
供 DataGrid 顯示LoadStocksCommand
非同步抓資料IsBusy
控制按鈕狀態與 LoadingErrorMessage
顯示錯誤// ViewModels/StockListViewModel.cs
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
public class StockListViewModel : INotifyPropertyChanged
{
private readonly IStockApiService _api;
private CancellationTokenSource _cts;
private bool _isBusy;
private string _errorMessage;
public ObservableCollection<StockProfile> Stocks { get; } = new ObservableCollection<StockProfile>();
public bool IsBusy
{
get => _isBusy;
private set { _isBusy = value; OnPropertyChanged(nameof(IsBusy)); }
}
public string ErrorMessage
{
get => _errorMessage;
private set { _errorMessage = value; OnPropertyChanged(nameof(ErrorMessage)); }
}
public ICommand LoadStocksCommand { get; }
public ICommand CancelCommand { get; }
public StockListViewModel(IStockApiService api)
{
_api = api ?? throw new ArgumentNullException(nameof(api));
LoadStocksCommand = new AsyncCommand(LoadStocksAsync, () => !IsBusy);
CancelCommand = new AsyncCommand(CancelAsync, () => IsBusy);
}
private async Task LoadStocksAsync()
{
ErrorMessage = null;
IsBusy = true;
_cts = new CancellationTokenSource();
try
{
var list = await _api.GetStocksAsync(_cts.Token);
Stocks.Clear();
foreach (var s in list)
Stocks.Add(s);
}
catch (OperationCanceledException)
{
ErrorMessage = "已取消下載。";
}
catch (Exception ex)
{
ErrorMessage = $"下載失敗:{ex.Message}";
}
finally
{
IsBusy = false;
_cts = null;
(LoadStocksCommand as AsyncCommand)?.RaiseCanExecuteChanged();
(CancelCommand as AsyncCommand)?.RaiseCanExecuteChanged();
}
}
private Task CancelAsync()
{
_cts?.Cancel();
return Task.CompletedTask;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Stocks
BooleanToVisibilityConverter
控制 Loading 顯示<!-- Views/MainWindow.xaml -->
<Window x:Class="MyStockApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="選股工具 - 資料下載" Height="500" Width="800">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="Bool2Vis"/>
</Window.Resources>
<DockPanel>
<!-- 工具列 -->
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top" Margin="8" HorizontalAlignment="Left" Spacing="8">
<Button Content="下載股票"
Command="{Binding LoadStocksCommand}"
IsEnabled="{Binding IsBusy, Converter={StaticResource Bool2Vis}, ConverterParameter=Invert}">
<!-- 上面這行若要更直覺可改用 Triggers 或多一個反向轉換器;此處簡化可先不綁定 IsEnabled -->
</Button>
<Button Content="取消"
Command="{Binding CancelCommand}"
IsEnabled="{Binding IsBusy}"/>
<ProgressBar Width="150" Height="16"
IsIndeterminate="True"
Visibility="{Binding IsBusy, Converter={StaticResource Bool2Vis}}"/>
</StackPanel>
<!-- 錯誤訊息 -->
<TextBlock Margin="8" Foreground="Tomato" Text="{Binding ErrorMessage}"/>
<!-- 資料表格 -->
<DataGrid ItemsSource="{Binding Stocks}" AutoGenerateColumns="False" Margin="8"
EnableRowVirtualization="True" IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="代號" Binding="{Binding Code}" Width="100"/>
<DataGridTextColumn Header="名稱" Binding="{Binding Name}" Width="200"/>
<DataGridTextColumn Header="產業" Binding="{Binding Industry}" Width="*"/>
<DataGridTextColumn Header="更新時間 (UTC)"
Binding="{Binding LastUpdatedUtc, StringFormat=\{0:yyyy-MM-dd HH:mm:ss\}}" Width="200"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Window>
說明
BooleanToVisibilityConverter
讓IsBusy
→Visibility
,顯示/隱藏進度條ObservableCollection
在新增/清除時,DataGrid 會自動刷新IsReadOnly="True"
確保資料表不被直接編輯(此處僅顯示)
在程式進入點或 MainWindow.xaml.cs
注入服務與 ViewModel:
// Views/MainWindow.xaml.cs
using System.Net.Http;
using System.Windows;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var http = new HttpClient(); // 正式專案建議使用 IHttpClientFactory
var api = new TwseStockApiService(http);
this.DataContext = new StockListViewModel(api);
}
}
await
的方法內IsBusy
、ErrorMessage
等狀態才會即時反映CancellationTokenSource
,提供「取消」功能ErrorMessage
顯示LoadStocksCommand.CanExecute
回傳 false
EnableRowVirtualization="True"
,大量列時較順暢今天把前面的知識整合成可用的 WPF 畫面:
ObservableCollection
→ DataGrid 即時顯示