不少朋友启动量化客户端项目时,第一反应不是搭工程,而是先拉控件画K线。这个冲动我完全理解,毕竟界面有反馈才有成就感。但以我做桌面客户端的经验,这类项目最容易翻车的点不在K线画得好不好,而在环境、配置文件、日志记录和基础框架这四件事没提前想清楚。等行情推送、交易回报、策略信号全涌进来的时候,你还在为日志缺失和依赖混乱的问题发愁,那才叫真正的难受。
这篇教程(001)是《基于WPF的量化客户端开发》系列第一篇,内容很聚焦:把开发环境、解决方案结构、配置体系、日志模块和MVVM基础骨架一次性落地,让你从第一行代码开始就是工程化的。它适合正在启动量化客户端项目的.NET开发者,也适合想从单窗体验证程序走向分层架构的朋友。读完这一篇,你会得到一个能继续写行情、交易、策略功能的稳定底座,而不是一堆演示用的Demo。
1. 选型与环境先行:WPF量化客户端的第一批决策
1.1 为什么是WPF:界面复杂度与生态的权衡
说WPF之前,先回答一个问题:量化客户端界面到底特殊在哪?它不是传统管理后台那种“表单+表格”就完事的系统,K线、分时、盘口、深度图、委托回报、策略监控,这些窗口里的数据每秒都有可能变化,而且价格区域需要高精度渲染。这就要求UI框架在自绘能力、数据绑定、异步刷新上有足够的底子。
WPF的矢量渲染、数据模板、样式系统恰好是为这些场景准备的。数据绑定可以把行情对象的属性直接映射到界面,INotifyPropertyChanged让价格变动自动更新界面,值转换器又能把价格方向转为颜色和箭头,这些在WinForms里也能做,但做起来很别扭,后期维护成本比你预想的高得多。
有朋友会问,为什么不选Avalonia或者MAUI?跨平台是听着很香,但量化客户端的核心使用场景就是Windows终端。再看第三方控件生态,成熟的网格控件、图表控件对WPF的支持度明显更高,这些在实盘界面上几乎都是刚需。跨平台换来的是控件选型受限、图表能力打折,我不觉得这笔账划算。
| 对比项 | WPF | WinForms | Avalonia | MAUI |
|---|---|---|---|---|
| 数据绑定与MVVM | 原生支持,成熟 | 弱,需大量手工代码 | 支持,生态较小 | 支持,主要面向移动端 |
| 自绘与自定义样式 | 强 | 弱 | 较强 | 一般 |
| 第三方桌面控件生态 | 丰富 | 一般 | 较少 | 较少 |
| 量化客户端适用度 | 高 | 中 | 低 | 低 |
所以第一批决策里的第一个答案就是:Windows环境,直接用WPF,别犹豫。框架层面我推荐.NET 8,这是当前长期支持版本,维护周期长,不追.NET 9这类尝鲜版。做量化客户端,稳定比什么都重要,界面框架不折腾本身就是一种收益。如果你还没有明确的团队约束,这个组合基本可以闭眼选。
1.2 开发环境清单:SDK、IDE与运行时版本
动手之前先把环境踩一遍,免得后面每一步都在跟环境问题搏斗。我的常规环境是这样的:操作系统用Windows 10或11的x64版本;.NET SDK选择8.0.x,一定装SDK而不是只装运行时,因为后面的dotnet命令行工具、MSBuild编译都依赖SDK。IDE用Visual Studio 2022,建议17.8以上版本,安装时勾选“.NET 桌面开发”工作负载,里面包含了WPF项目模板、XAML设计器和专门的调试工具。
版本管理工具Git也要提前就位。量化客户端虽然通常不像后端服务那么大,但配置文件模板、策略参数、发布脚本都需要纳入版本管理,否则改了什么、什么时候改的很快就说不清了。
第一次搭建环境时有件事,我专门提醒一下:把项目路径、解决方案目录尽量保持在英文字符环境下。我有一次把项目放在一个带中文和空格的目录下,某些第三方原生控件在加载资源时总出诡异问题,折腾了半天,最后把路径改成纯英文就好了。这种坑能避开就避开,别把时间花在不该花的地方。
装完之后,打开命令行执行dotnet --list-sdks,能看到8.0.x的版本号,说明SDK已经就绪。如果你是第一次跑WPF项目,建议先新建一个空模板编译一次,确认XAML编译链路没有问题,再进入正式的解决方案搭建。环境如果在这里没理顺,后面每新建一个项目都会带着同一个隐患走。顺便把NuGet默认源确认一下,保证基础包能正常拉取,这一步省不了。
1.3 NuGet依赖边界:不贪多,只拿必须的
量化客户端这种项目,很忌讳一上来就把依赖堆得满满的。我的习惯是,第一节课只引入五个核心包,其余等用到时再评估。下面是这个阶段最小的依赖清单:
| NuGet包 | 用途 | 引入时机 |
|---|---|---|
| CommunityToolkit.Mvvm | MVVM基础:ObservableObject、RelayCommand等 | 现在 |
| NLog | 日志记录 | 现在 |
| Microsoft.Extensions.Configuration.Json | 读取appsettings.json | 现在 |
| Microsoft.Extensions.Configuration.Binder | 配置类强类型绑定 | 现在 |
| Microsoft.Extensions.DependencyInjection | 依赖注入容器 | 现在 |
有人会问,第三方UI控件为什么不上?各种现成的表格、图表控件确实功能强,但会带来授权方式不同、主题定制受限、更新节奏不可控的问题。而且它们往往锁定你的界面架构,以后想套一套自定义Dark主题,得绕不少弯路。我的建议是先让基础框架独立于第三方控件库,K线图这部分等后面单独评估,那时候你会更清楚自己到底需要什么样的绘制能力。
包版本方面也不要盲目追最新,用当前稳定的正式版即可。CommunityToolkit.Mvvm和NLog的版本迭代都相对平稳,安装时只要注意目标框架版本匹配就行。依赖这一栏的原则是宁缺毋滥,它决定了你后面维护时的精神状态。
2. 初始化解决方案:一个量化客户端该有的项目边界
2.1 四层架构划分:从界面到基础设施的分工
量化客户端虽然是桌面程序,但业务复杂度一点不比后端服务低。行情、账户、订单、策略、风控,再加上日志、配置、持久化,如果全塞进一个项目,早期可能觉得快,后面每次加功能你都会经历一次“改这里崩那里”的循环。所以我在这个阶段就划分四个项目,把边界彻底画清楚。
本系列采用四层划分,不是严格的DDD,但对客户端来说非常够用:
- 展示层(QuantDesktop.UI):只放XAML界面和ViewModel,不写业务实现。
- 应用层(QuantDesktop.Application):定义用例和接口,比如连接行情、提交订单、启动策略,它是UI和底层的中转站。
- 领域层(QuantDesktop.Domain):放核心模型,订单、成交、账户、策略信号,这类对象与具体UI和接入方式无关。
- 基础设施层(QuantDesktop.Infrastructure):真正干活的实现,比如NLog写日志、行情API封装、数据库访问。
我见过很多从单项目迁移过来的同学,最大的痛苦是不知道某个类该往哪放。四层划分之后,规则变得很简单:界面把命令发给应用层,应用层调领域模型,基础设施层提供各种真实服务。这算不上什么高深理论,只是给每个类安排一个固定房号,让项目结构清晰可预期,后续新同学接手也能快速找到代码位置。
2.2 项目创建细节:目标框架与项目类型
创建项目的过程不复杂,但有几步很容易忽略。我的顺序是先用解决方案做总入口,名称比如QuantDesktop.sln,然后在里面新建四个项目,UI项目放在最外层作为启动项目。
关键设置是:三个类库项目和WPF主项目统一使用net8.0-windows目标框架。主项目QuantDesktop.UI的输出类型是WinExe,其余类库是Library,因为WPF程序需要带-windows后缀,其他类库为了以后引用Windows兼容API时不至于改框架,我也统一用这个后缀。如果编译环境不是Windows,还需要在csproj里设置EnableWindowsTargeting为true,这条属于冷知识,但我在一些自动化构建环境里遇到过,列出来备查。
下面是UI项目的csproj核心片段:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net8.0-windows</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <UseWPF>true</UseWPF> <AssemblyName>QuantDesktop.UI</AssemblyName> </PropertyGroup> </Project>其他类库项目把OutputType删掉、UseWPF设为false就行。命名空间我直接按项目名划分,保持一对一,后续找代码非常直接。像QuantDesktop.Infrastructure里只需要包含“实现”类,不掺任何XAML相关的东西。
2.3 依赖方向与项目引用红线
项目边界出来后,最重要的一条红线是依赖方向。我允许的引用关系如下:
- UI项目引用Application、Domain、Infrastructure项目
- Application项目引用Domain项目
- Infrastructure项目引用Application和Domain项目
- UI项目通过DI在入口处注入Infrastructure的实现,ViewModel里从不直接new具体服务
说白了,Domain是最底层的积木,谁都能引用它;Application定义能力清单;Infrastructure是实现清单;UI是最终把所有清单组合起来的入口。这种依赖方向一旦混了,比如Infrastructure去引用UI,或者Application去引用Infrastructure,必然会出现循环引用,编译直接报错。
我踩过最典型的坑是:写日志的时候想把界面上的某个状态也一起打出来,就在Infrastructure里引用了UI项目。结果编译告诉你循环引用,我只好停下来重新设计,最后把那个状态通过参数传到应用层再打日志,问题才解决。这个教训说明,越界之后最难受的不是改代码,而是整个结构忽然不干净了。所以一开始守住红线,后面每个模块都是在一个稳定的结构里添加内容,不会再为架构问题分心。
3. 配置文件体系:把环境差异隔绝在代码之外
3.1 放弃App.config,拥抱appsettings.json
在.NET Framework时代,WPF项目默认会有App.config,XML格式,用ConfigurationManager去读,写起来啰嗦,多环境切换更是靠手动替换节点。我的建议是,不管旧习惯多顺手,新项目里直接用appsettings.json,配合Microsoft.Extensions.Configuration这套体系,这才是当前.NET生态里更自然的配置玩法。
量化客户端需要配置的东西其实不少:行情服务器地址、交易服务器地址、模拟盘/实盘标识、日志级别、数据库连接串、策略参数默认值。用JSON表达这些比XML清晰得多,嵌套结构一目了然,字段还能直接映射到C#强类型对象。下面是我在这个项目里的一版初始配置:
{ "App": { "Name": "QuantDesktop", "Environment": "Development" }, "Trading": { "QuoteServer": "tcp://quote.example:9000", "TradeServer": "tcp://trade.example:9001", "Mode": "Simulation", "AccountId": "DEMO001" }, "Logging": { "Level": "Info" } }这里我要特别提醒一句:任何涉及实盘账户真实凭据的信息,都不应该以明文形式提交到版本库里。配置文件里的账户和地址,我习惯用环境变量或者部署阶段生成的文件来覆盖,避免真实信息留在源代码仓库中。这不是工程洁癖,而是底线问题,出了问题就不是改代码能解决的了。
3.2 配置文件的拷贝行为与多环境切换
appsettings.json放在UI项目里之后,还要做一件事:确保它在编译时被复制到输出目录。在csproj里加一个ItemGroup,或者直接在文件属性里把“复制到输出目录”设为“如果较新则复制”。如果忘了这一步,程序运行时会静默找不到配置文件,而你大概率会先怀疑配置代码写错了,最后才会意识到文件根本没进运行目录。
<ItemGroup> <None Update="appsettings.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup>多环境切换方面,我采用文件覆盖模式:appsettings.json放公共配置,appsettings.Development.json和appsettings.Production.json放局部差异。程序启动时根据当前环境加载对应文件。桌面程序没有Web那种天然的环境变量体系,我用DOTNET_ENVIRONMENT环境变量来区分,同时也可以用命令行参数覆盖,这样打包给不同用户时更灵活。
启动时组装配置的代码如下:
var environment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"; var configuration = new ConfigurationBuilder() .SetBasePath(AppContext.BaseDirectory) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build();注意一下优先级:后面加载的会覆盖前面加载的同名键,环境变量在最外层,适合覆盖连接串这类部署期设置。reloadOnChange设为true,客户端虽然很少热重载配置,但保留这个能力没坏处,万一以后要做运行时设置面板,这套配置体系可以直接复用。
3.3 强类型配置绑定:让配置不再裸奔
有些项目里到处是configuration["Trading:QuoteServer"]这种取法。短期没问题,一旦配置键改了名或者漏了一处没改,编译期根本发现不了,运行期才爆炸。我的做法是定义强类型配置类,把JSON里的一段绑定成C#对象。
例如Trading配置绑定到TradingOptions:
public sealed class TradingOptions { public string QuoteServer { get; set; } = string.Empty; public string TradeServer { get; set; } = string.Empty; public string Mode { get; set; } = "Simulation"; public string AccountId { get; set; } = string.Empty; }在组合根注册的时候,把配置section绑定进去:
services.Configure<TradingOptions>(configuration.GetSection("Trading")); services.Configure<LoggingOptions>(configuration.GetSection("Logging"));以后服务类只要在构造函数里接收IOptions ,就能安全地读写配置。配置键变更时,顶多在这一个类里调整,不会牵连十几个地方。这是我特别推荐的一个小习惯,它把“配置”从魔法字符串变成了可编译检查的真实类型,排错成本低了一个量级。
4. 日志记录模块:让程序从一开始就会说话
4.1 日志框架怎么选:NLog与Serilog的取舍
日志模块在量化客户端里的地位,怎么说都不过分。行情实时推送、委托回报、账户变化,再加上策略开仓平仓,这些事件的时间线就是你需要还原的程序运行脉络。没有日志,K线崩了你都不知道价格数据是从哪个环节断掉的。
市面上的.NET日志框架里,我实际对比过三个:NLog、Serilog、Log4Net。Serilog的优势是结构化日志,能把日志事件转成JSON送给日志中心,适合微服务架构,但对单机客户端来说略显重,部署一套日志收集系统也不是小成本。Log4Net曾经很经典,但维护频率下降,配置写起来也更繁琐,新项目我一般不推荐。NLog的好处是配置简单、按logger名字规则路由非常灵活、滚动文件日志开箱即用,而且改配置不用改代码,直接编辑NLog.config再重启就生效,对量化客户端这种“长时间运行、需要快速翻日志”的场景非常合适。
| 对比项 | NLog | Serilog | Log4Net |
|---|---|---|---|
| 配置方式 | XML | 代码或JSON | XML |
| 文件滚动 | 成熟 | 成熟 | 一般 |
| 结构化日志 | 一般 | 强 | 弱 |
| 单机客户端适用度 | 高 | 中 | 中 |
所以这个项目日志框架我选了NLog。这里多说一句,日志框架选定之后,尽量把具体框架隔离在一个封装层里,别让业务代码到处使用特定框架的静态API。后面就算要换框架,改动面也小,这是长期维护的基本觉悟。
4.2 NLog配置实战:控制台、文件与滚动
先安装NLog包,然后在UI项目添加一个NLog.config,记得把文件属性里的“复制到输出目录”设为“如果较新则复制”。一个基础的NLog.config大概是这样的:
<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true" throwConfigExceptions="true"> <targets> <target xsi:type="File" name="file" fileName="${basedir}/logs/${shortdate}.log" layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}" archiveAboveSize="10485760" archiveNumbering="Rolling" maxArchiveFiles="30" encoding="utf-8" /> <target xsi:type="Console" name="console" layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}" /> </targets> <rules> <logger name="*" minlevel="Info" writeTo="file,console" /> <logger name="Microsoft.*" minlevel="Warn" final="true" /> </rules> </nlog>几个参数说明一下:fileName里用了${shortdate},日志按天分文件,复盘的时候只要找当天文件就行;archiveAboveSize控制单个文件超过10MB就滚动归档,maxArchiveFiles保留最近30个,防止日志把磁盘撑爆。规则里最后一行把Microsoft.*第三方日志降到Warn,否则系统内部日志会刷屏,真正有用的业务日志反而被淹没。
如果你担心UI点击时被日志IO卡顿,可以在File target外面套一层AsyncWrapper:
<target name="asyncFile" xsi:type="AsyncWrapper" queueLimit="5000" overflowAction="Discard"> <target xsi:type="File" name="file" fileName="${basedir}/logs/${shortdate}.log" ... /> </target>异步日志让写入不阻塞UI线程,量化客户端高频事件多的时候很有用。queueLimit设5000,超过就丢弃而不是阻塞,保证交易主流程不被日志拖死。我之前在实盘环境里见过日志把线程池耗尽的情况,加了异步包装之后明显改善,这类配置还是值得认真对待的。
4.3 日志封装的正确姿势:别把Logger用成全局静态
有了NLog配置之后,怎么在代码里调用也有讲究。最容易犯的错是在每个类里写死一个静态Logger,像下面这样:
private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger();这种写法本身能用,但缺点在于没法针对业务模块动态调整日志级别,也跟依赖注入容器没有关系。在量化客户端里,我更推荐在组合根里注册NLog的日志提供程序:
services.AddLogging(builder => { builder.ClearProviders(); builder.AddNLog(); });这样业务类在构造函数里接收ILogger ,框架会自动带上当前类名作为logger名字,正好命中NLog.config里的规则路由。核心好处是日志上下文更准确,排障时一眼知道是哪个服务打印的,而不是所有日志都堆在同一个静态Logger下。
日志规范这块,我给自己定了几条,写代码的时候会下意识遵守:下单、撤单、成交这类资金敏感操作必须打日志,而且带上订单ID、合约代码、价格数量等上下文;异常日志必须连同StackTrace一起打出来,我通常用logger.Error(ex, "订单失败,订单ID={OrderId}", orderId)这种占位符写法,NLog会把它格式化易读;严禁在日志里记录密码、token、完整账户凭据。这个很多人知道,但忙起来容易忘记,所以我在项目里加了一份代码审查清单,专门检查日志内容,发布前基本能拦住大部分敏感信息。
5. 基础框架骨架:MVVM与程序入口的组合
5.1 量化客户端为什么绕不开MVVM
这一节开始进入框架核心。如果你是WPF新手,可能会问:为什么量化客户端一定要MVVM?其实不是“一定要”,而是“不用你后面会后悔”。最现实的原因在于,行情推送和委托回报是持续的异步事件,UI要同时更新价格、持仓、日志、状态栏等多个区域。如果你在代码里到处写TextBox.Text = xxx,最后会得到一个根本无法维护的耦合体。
MVVM的核心理念是:界面是数据的状态映射,页面上的变化都源于ViewModel里的属性变化。价格变了,只需改Price属性,绑定它的所有控件自动更新;用户点按钮,只需执行Command,具体逻辑在ViewModel里。UI和业务彻底解耦,后续界面改版不会影响业务逻辑。
WPF对MVVM是天生的支持:DataContext做绑定上下文,INotifyPropertyChanged做属性通知,DataTemplate把数据自动渲染成控件。这些能力配合量化场景,正好把高频数据流和界面刷新解耦开。比如行情价格变动时,你只需要在后台线程里更新某个属性,WPF的数据绑定会自动把界面刷新安排得明明白白,不需要你手动调用繁琐的控件更新方法。
5.2 ViewModelBase和RelayCommand:最小基础设施
虽然我们用CommunityToolkit.Mvvm,不需要自己造轮子,但理解它做什么仍然有价值。最常用的两个基础设施是ObservableObject和RelayCommand。前者提供SetProperty来完成属性通知,后者把一个方法包装成ICommand,供XAML里的按钮、菜单、快捷键绑定。
以连接行情服务的MainViewModel为例:
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; public sealed partial class MainViewModel : ObservableObject { [ObservableProperty] private string _connectionState = "未连接"; [ObservableProperty] private bool _isConnecting; [RelayCommand] private async Task ConnectAsync() { if (IsConnecting) return; IsConnecting = true; ConnectionState = "连接中..."; try { await Task.Delay(500); // 后续替换为真实连接逻辑 ConnectionState = "已连接"; } catch (Exception ex) { ConnectionState = "连接失败"; Logger.LogError(ex, "连接行情服务失败"); } finally { IsConnecting = false; } } }这里的[ObservableProperty]和[RelayCommand]是源生成器的功劳,编译时会自动生成属性通知和ICommand字段,不用手写几十行样板代码。ConnectCommand在XAML里直接绑定即可,AsyncRelayCommand天生支持异步方法,避免了async void这种危险写法。这时候注意,类声明必须是partial,这是源生成器的要求,别改成普通class,否则代码直接编译不过。
对于更复杂的界面,比如K线图上的价格显示,你可以配合值转换器IValueConverter把涨跌方向映射成颜色或箭头。这部分后续文章会专门展开,但MVVM基础设施已经足够支撑这些扩展。
5.3 组合根:从App启动到第一个MainWindow
MVVM框架里有一个“组合根”的概念,指的是程序最早创建依赖关系的地方。WPF里对应的就是App.xaml.cs的OnStartup方法。我不建议在App.xaml里写StartupUri让框架直接启动窗口,因为那样MainWindow就无法从容器的角度获取依赖和配置。正确的做法是重写OnStartup,先组装ServiceCollection再创建窗口:
public partial class App : Application { private IServiceProvider? _serviceProvider; protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var configuration = BuildConfiguration(); var services = new ServiceCollection(); services.AddSingleton(configuration); services.AddLogging(builder => { builder.ClearProviders(); builder.AddNLog(); }); services.Configure<TradingOptions>(configuration.GetSection("Trading")); services.AddSingleton<MainViewModel>(); services.AddSingleton<IQuoteService, MockQuoteService>(); _serviceProvider = services.BuildServiceProvider(); var mainWindow = new MainWindow { DataContext = _serviceProvider.GetRequiredService<MainViewModel>() }; MainWindow = mainWindow; mainWindow.Show(); } private static IConfiguration BuildConfiguration() { var environment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"; return new ConfigurationBuilder() .SetBasePath(AppContext.BaseDirectory) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build(); } protected override void OnExit(ExitEventArgs e) { LogManager.Shutdown(); base.OnExit(e); } }这段代码看着不长,但它是整个客户端的发动机启动过程:配置文件先加载、日志系统先就绪、业务服务注册,MainWindow创建时通过构造函数注入所有依赖。之后MainViewModel里需要行情服务、日志、配置,全部从构造函数来,不用在任何地方手动定位服务。App.xaml里只需要把StartupUri那一行删掉,因为窗口由代码创建。ShutdownMode默认OnLastWindowClose,单主窗口项目用MainWindow显式赋一下就行。
5.4 给后续行情模块预留的接入点
框架搭到这儿,其实已经可以做第一个有意义的垂直切片了。我在应用层定义一个行情接口:
public interface IQuoteService { Task ConnectAsync(CancellationToken cancellationToken); event Action<QuoteTick>? QuoteReceived; }基础设施层先提供一个Mock实现:
public sealed class MockQuoteService : IQuoteService { private readonly ILogger<MockQuoteService> _logger; public MockQuoteService(ILogger<MockQuoteService> logger) { _logger = logger; } public async Task ConnectAsync(CancellationToken cancellationToken) { _logger.LogInformation("Mock行情服务开始连接"); await Task.Delay(100, cancellationToken); _logger.LogInformation("Mock行情服务连接完成"); } public event Action<QuoteTick>? QuoteReceived; }UI层的MainViewModel在ConnectCommand里调用IQuoteService,而到底用Mock还是真实实现,全部由组合根里的services.AddSingleton<IQuoteService, xxx>()决定。这样后续教程做真实行情接入时,只需要新增一个RealQuoteService并改一行注册代码,UI和应用层完全不用动。
我自己的习惯是,第一节课搭地基宁可多花两天,也不要急着画K线。环境、配置、日志、MVVM骨架只要稳固,后面每个功能模块都能在一个干净的骨架上生长,排障速度比“项目先跑了再补课”的模式快得多。下一篇教程里,我们就可以在这个基础上接行情数据,让K线真正动起来。