WinForms应用中基于Timer的可重试地理编码请求与CancellationToken处理
实现Windows Forms地址输入后延迟地理编码并支持取消旧请求
这个需求我之前在Windows Forms项目里落地过,核心是用定时器实现输入后的延迟触发,同时借助取消令牌机制中断正在进行的旧请求,下面是完整的实现方案:
核心思路
- 用
System.Timers.Timer设置延迟(比如3秒),用户每次修改地址字段时重置定时器 - 用
CancellationTokenSource管理HTTP请求的生命周期,用户修正输入时立即取消当前请求 - 注意线程安全:
System.Timers.Timer的事件在后台线程触发,访问UI控件必须切换回UI线程
具体实现步骤
1. 定义类级变量
在Form类中声明需要的核心对象,用于线程同步和请求管理:
private System.Timers.Timer _geocodeTimer; private CancellationTokenSource _currentCts; private readonly object _timerLock = new object(); // 用于多线程操作的同步锁
2. 初始化定时器
在Form的Load事件或构造函数中完成定时器的配置:
private void AddressForm_Load(object sender, EventArgs e) { // 设置延迟时间(3000毫秒=3秒,可根据需求调整) _geocodeTimer = new System.Timers.Timer(3000); _geocodeTimer.AutoReset = false; // 只触发一次,每次输入变化手动重启 _geocodeTimer.Elapsed += GeocodeTimer_Elapsed; // 绑定触发事件 }
3. 监听地址字段的输入变化
给所有地址输入框(街道、城市、州、邮编)绑定TextChanged事件,每次输入变化时重置定时器并取消旧请求:
private void AddressField_TextChanged(object sender, EventArgs e) { lock (_timerLock) { // 取消当前正在进行的请求(如果存在) _currentCts?.Cancel(); // 停止当前定时器 _geocodeTimer.Stop(); // 创建新的取消令牌源,用于后续的请求 _currentCts = new CancellationTokenSource(); // 重启定时器,等待用户停止输入后触发地理编码 _geocodeTimer.Start(); } }
4. 定时器触发时调用地理编码API
实现定时器的Elapsed事件处理函数,在这里完成API调用和UI更新:
private async void GeocodeTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { CancellationToken token; lock (_timerLock) { if (_currentCts == null) return; token = _currentCts.Token; } try { // 从UI控件获取地址信息(必须用Invoke切换回UI线程) string street = Invoke(new Func<string>(() => txtStreet.Text.Trim())); string city = Invoke(new Func<string>(() => txtCity.Text.Trim())); string state = Invoke(new Func<string>(() => txtState.Text.Trim())); string zip = Invoke(new Func<string>(() => txtZip.Text.Trim())); // 验证地址完整性,避免无效请求 if (string.IsNullOrEmpty(street) || string.IsNullOrEmpty(city) || string.IsNullOrEmpty(state)) { Invoke(new Action(() => lblStatus.Text = "地址信息不完整,请补充")); return; } // 调用US Census地理编码API var geocodeResult = await CallUSCensusGeocodeApi(street, city, state, zip, token); // 更新UI显示结果(切换回UI线程) Invoke(new Action(() => { lblLatitude.Text = $"纬度: {geocodeResult.Latitude:F6}"; lblLongitude.Text = $"经度: {geocodeResult.Longitude:F6}"; lblStatus.Text = "地理编码完成"; })); } catch (OperationCanceledException) { // 请求被用户输入中断,无需额外处理 Invoke(new Action(() => lblStatus.Text = "请求已取消")); } catch (Exception ex) { // 处理API调用错误 Invoke(new Action(() => lblStatus.Text = $"编码失败: {ex.Message}")); } finally { lock (_timerLock) { // 释放取消令牌源资源,避免内存泄漏 _currentCts?.Dispose(); _currentCts = null; } } }
5. 实现US Census地理编码API调用
编写异步方法调用US Census的地理编码API,支持取消令牌:
private async Task<GeocodeResult> CallUSCensusGeocodeApi(string street, string city, string state, string zip, CancellationToken token) { using (var client = new HttpClient()) { // 构建API请求URL,注意转义特殊字符 string apiUrl = $"https://geocoding.geo.census.gov/geocoder/locations/address" + $"?street={Uri.EscapeDataString(street)}" + $"&city={Uri.EscapeDataString(city)}" + $"&state={Uri.EscapeDataString(state)}" + $"&zip={Uri.EscapeDataString(zip)}" + $"&benchmark=Public_AR_Current&format=json"; // 发送GET请求并传入取消令牌 using (var response = await client.GetAsync(apiUrl, token)) { response.EnsureSuccessStatusCode(); // HTTP状态码非200时抛出异常 string jsonResponse = await response.Content.ReadAsStringAsync(token); // 解析JSON响应,模型类对应US Census的返回结构 var apiResponse = JsonSerializer.Deserialize<USCensusGeocodeResponse>(jsonResponse); // 提取第一个匹配结果的经纬度 var firstMatch = apiResponse?.Result?.AddressMatches?.FirstOrDefault(); return new GeocodeResult { Latitude = firstMatch?.Coordinates?.Y ?? 0, Longitude = firstMatch?.Coordinates?.X ?? 0 }; } } } // 用于解析API响应的模型类 public class GeocodeResult { public double Latitude { get; set; } public double Longitude { get; set; } } public class USCensusGeocodeResponse { public USCensusResult Result { get; set; } } public class USCensusResult { public List<USCensusAddressMatch> AddressMatches { get; set; } } public class USCensusAddressMatch { public USCensusCoordinates Coordinates { get; set; } } public class USCensusCoordinates { public double X { get; set; } // 经度 public double Y { get; set; } // 纬度 }
关键注意事项
- 线程安全:
System.Timers.Timer的Elapsed事件在后台线程执行,所有对UI控件的操作必须用Invoke或BeginInvoke切换回UI线程 - 资源释放:每次请求完成或取消后,必须
Dispose掉CancellationTokenSource,避免内存泄漏 - AutoReset设置:将定时器的
AutoReset设为false,确保每次输入变化只会触发一次地理编码请求 - 输入验证:在调用API前验证地址字段的完整性,减少无效请求
内容的提问来源于stack exchange,提问作者Sean
相关产品推荐
相关产品推荐

