IServiceProvider
HttpClientFactory
,HTTP 呼叫更穩定為了強調「資料層門面」的角色,我們把介面統一為 IDatabaseRepository
。
實作細節(LiteDB)今天不展開;你只要知道 ViewModel 只依賴 IDatabaseRepository,容器會提供實作。
在 WPF 專案 MyStockApp 安裝以下 NuGet:
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Http
<!-- MyStockApp/App.xaml -->
<Application x:Class="MyStockApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- 不指定 StartupUri,改由 App.xaml.cs 控制 -->
</Application>
// MyStockApp/App.xaml.cs
using System;
using System.Windows;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System.Net.Http;
namespace MyStockApp
{
public partial class App : Application
{
private IHost _host;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
_host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
// 1) DB 門面
services.AddSingleton<IDatabaseRepository, LiteDbStockRepository>(sp =>
new LiteDbStockRepository("StockData.db")); // 路徑可抽設定
// 2) HttpClientFactory + API 服務(Typed Client)
services.AddHttpClient<IStockApiService, TwseStockApiService>(client =>
{
// client.BaseAddress = new Uri("https://openapi.twse.com.tw/");
// 可設定逾時、Header 等
client.Timeout = TimeSpan.FromSeconds(30);
});
// 3) 資料抓取 / 指標計算 / 批次工作
services.AddSingleton<IQuoteFetchService, QuoteFetchService>();
services.AddSingleton<IIndicatorService, IndicatorService>();
services.AddSingleton<IBuildIndicatorsJob, BuildIndicatorsJob>();
// 4) ViewModel 與 View
services.AddTransient<StockFilterViewModel>(); // 每次新建一個 VM
services.AddTransient<MainWindow>(); // 由容器建立並注入 VM
})
.Build();
// 從容器取得 MainWindow(其依賴會自動注入)
var main = _host.Services.GetRequiredService<MainWindow>();
main.Show();
}
protected override async void OnExit(ExitEventArgs e)
{
if (_host is not null) await _host.StopAsync();
_host?.Dispose();
base.OnExit(e);
}
}
}
GetDailyQuotes(code, start, end)
從 ICrawler 移過來)以上在 Day 24 已完成;今天只是把它們註冊進 DI。
// MyStockApp/MainWindow.xaml.cs
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
namespace MyStockApp
{
public partial class MainWindow : Window
{
public MainWindow(StockFilterViewModel vm)
{
InitializeComponent();
DataContext = vm; // 由 DI 傳入,解除 new 依賴
}
}
}
<!-- MyStockApp/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="MyStockApp" Width="900" Height="600">
<DockPanel>
<!-- 你可以把 Day 23 的樣式 / Day 24 的表格延續使用 -->
<ContentControl Content="{Binding}">
<!-- 綁定的是 StockFilterViewModel,DataContext 已在 code-behind 設好 -->
</ContentControl>
</DockPanel>
</Window>
重點:
- 依賴 IDatabaseRepository(容器注入)
- 提供「載入股票」「重整指標」「清除條件」命令
- 篩選:關鍵字 / 指定日期的 KD 黃金交叉 / MACD 柱正
- 以快取
_indicatorCache
減少資料庫打點
// MyStockApp/ViewModel/StockFilterViewModel.cs
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Data;
using System.Windows.Input;
namespace MyStockApp
{
public class StockFilterViewModel : INotifyPropertyChanged
{
private readonly IDatabaseRepository _repo;
public StockFilterViewModel(IDatabaseRepository repo)
{
_repo = repo ?? throw new ArgumentNullException(nameof(repo));
View = CollectionViewSource.GetDefaultView(Stocks);
View.Filter = FilterPredicate;
_selectedDate = DateTime.Now.Date;
LoadFromLocalCommand = new AsyncCommand(LoadFromLocalAsync);
RefreshIndicatorsCommand = new AsyncCommand(RefreshIndicatorCacheAsync, () => Stocks.Count > 0);
ClearFiltersCommand = new RelayCommand(_ =>
{
Query = string.Empty;
OnlyKdGoldenCross = false;
OnlyMacdPositive = false;
});
}
// ===== 資料源 =====
public ObservableCollection<StockProfile> Stocks { get; } = new();
public ICollectionView View { get; }
// ===== 條件 =====
private string _query = string.Empty;
public string Query
{
get => _query;
set { _query = value ?? string.Empty; OnPropertyChanged(nameof(Query)); View.Refresh(); }
}
private DateTime _selectedDate;
public DateTime SelectedDate
{
get => _selectedDate;
set { _selectedDate = value.Date; OnPropertyChanged(nameof(SelectedDate)); _ = RefreshIndicatorCacheAsync(); }
}
private bool _onlyKdGoldenCross;
public bool OnlyKdGoldenCross
{
get => _onlyKdGoldenCross;
set { _onlyKdGoldenCross = value; OnPropertyChanged(nameof(OnlyKdGoldenCross)); View.Refresh(); }
}
private bool _onlyMacdPositive;
public bool OnlyMacdPositive
{
get => _onlyMacdPositive;
set { _onlyMacdPositive = value; OnPropertyChanged(nameof(OnlyMacdPositive)); View.Refresh(); }
}
// ===== 指令 =====
public ICommand LoadFromLocalCommand { get; }
public ICommand RefreshIndicatorsCommand { get; }
public ICommand ClearFiltersCommand { get; }
// ===== 指標快取(指定日)=====
private readonly Dictionary<string, DailyIndicator?> _indicatorCache = new();
private async Task LoadFromLocalAsync()
{
Stocks.Clear();
var profiles = await _repo.QueryProfilesAsync(); // 由資料層提供主檔清單
foreach (var p in profiles) Stocks.Add(p);
await RefreshIndicatorCacheAsync();
}
private async Task RefreshIndicatorCacheAsync()
{
_indicatorCache.Clear();
foreach (var s in Stocks)
{
var ind = await _repo.GetIndicatorAtOrBeforeAsync(s.Code, SelectedDate);
_indicatorCache[s.Code] = ind;
}
(RefreshIndicatorsCommand as AsyncCommand)?.RaiseCanExecuteChanged();
View.Refresh();
}
private bool FilterPredicate(object obj)
{
if (obj is not StockProfile s) return false;
if (!string.IsNullOrWhiteSpace(Query))
{
var q = Query.Trim();
if (!s.Code.Contains(q, StringComparison.OrdinalIgnoreCase) &&
!s.Name.Contains(q, StringComparison.OrdinalIgnoreCase))
return false;
}
if (OnlyKdGoldenCross || OnlyMacdPositive)
{
if (!_indicatorCache.TryGetValue(s.Code, out var ind) || ind is null) return false;
if (OnlyKdGoldenCross && !(ind.K.HasValue && ind.D.HasValue && ind.K.Value > ind.D.Value))
return false;
if (OnlyMacdPositive && !(ind.MACDHist.HasValue && ind.MACDHist.Value > 0m))
return false;
}
return true;
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string name)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
上例使用的
StockProfile
、DailyIndicator
、AsyncCommand
、RelayCommand
請沿用專案內既有定義。
如果IDatabaseRepository
方法名稱不同(例如GetIndicatorAtOrBeforeAsync
),請對照你的介面做小調整即可。
IDatabaseRepository
、IndicatorService
、QuoteFetchService
、BuildIndicatorsJob
→ Singleton
StockFilterViewModel
、MainWindow
→ Transient
IStockApiService
→ AddHttpClient(背後用 HttpClientFactory
)