
在现代桌面应用开发中,前后端分离早已不是 Web 专属。WPF 作为成熟的 UI 框架,搭配 ASP.NET Core WebApi 作为服务端,既能发挥桌面端丰富的交互能力,又能利用 WebApi 实现业务逻辑统一、多客户端复用。本文将带你从零搭建一套完整的“WPF 客户端 + WebApi 服务端”应用程序,涵盖项目初始化、接口开发、数据库集成、JWT 认证、WPF MVVM 架构、HttpClient 调用、异步命令、错误处理及最终部署,所有代码均基于 .NET 8/9,适合中级开发者实战参考。
本文不依赖第三方 MVVM 框架(如 Prism),而是使用 .NET 内置的 CommunityToolkit.MVVM 来简化开发,但也会提到如何替换为 Prism。
我们创建一个 Visual Studio 解决方案,包含三个项目:
Solution: MyApp
├── MyApp.WebApi (ASP.NET Core WebApi 项目)
├── MyApp.Client (WPF 应用程序)
└── MyApp.Shared (类库,存放 DTO、枚举、常量等共享类型)使用 dotnet CLI 或 VS 创建:
dotnet new sln -n MyApp
dotnet new webapi -n MyApp.WebApi
dotnet new wpf -n MyApp.Client
dotnet new classlib -n MyApp.Shared
dotnet sln add MyApp.WebApi/MyApp.WebApi.csproj
dotnet sln add MyApp.Client/MyApp.Client.csproj
dotnet sln add MyApp.Shared/MyApp.Shared.csproj为项目添加引用:
MyApp.WebApi 引用 MyApp.SharedMyApp.Client 引用 MyApp.SharedMyApp.Client 添加包:CommunityToolkit.MVVM、Microsoft.Extensions.Http、Microsoft.Extensions.DependencyInjection在 MyApp.Shared 中定义 DTO 和枚举,确保服务端和客户端使用相同的类型。
// MyApp.Shared/DTOs/UserDto.cs
namespace MyApp.Shared.DTOs;
public class UserDto
{
public int Id { get; set; }
public string UserName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string? Avatar { get; set; }
}
public class LoginRequest
{
public string UserName { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
public class LoginResponse
{
public string Token { get; set; } = string.Empty;
public UserDto User { get; set; } = null!;
}// MyApp.Shared/Responses/ApiResponse.cs
namespace MyApp.Shared.Responses;
public class ApiResponse<T>
{
public bool Success { get; set; }
public string? Message { get; set; }
public T? Data { get; set; }
public int StatusCode { get; set; }
public static ApiResponse<T> Ok(T data, string message = "成功") =>
new() { Success = true, Message = message, Data = data, StatusCode = 200 };
public static ApiResponse<T> Fail(string message, int statusCode = 400) =>
new() { Success = false, Message = message, StatusCode = statusCode };
}// MyApp.Shared/Constants/ApiConstants.cs
namespace MyApp.Shared.Constants;
public static class ApiConstants
{
public const string BaseUrl = "https://localhost:5001"; // 开发环境
public const string LoginEndpoint = "/api/auth/login";
public const string UsersEndpoint = "/api/users";
}修改 appsettings.json:
{
"Logging": { "LogLevel": { "Default": "Information" } },
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyAppDb;Trusted_Connection=True;"
},
"Jwt": {
"Key": "your-very-long-secret-key-at-least-32-chars",
"Issuer": "MyApp",
"Audience": "MyAppClient",
"ExpireMinutes": 60
},
"AllowedHosts": "*"
}dotnet add MyApp.WebApi package Microsoft.EntityFrameworkCore.SqlServer
dotnet add MyApp.WebApi package Microsoft.EntityFrameworkCore.Tools
dotnet add MyApp.WebApi package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add MyApp.WebApi package Swashbuckle.AspNetCore创建实体和 DbContext:
// MyApp.WebApi/Models/User.cs
namespace MyApp.WebApi.Models;
public class User
{
public int Id { get; set; }
public string UserName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string PasswordHash { get; set; } = string.Empty; // 实际应使用BCrypt
public string? Avatar { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}// MyApp.WebApi/Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using MyApp.WebApi.Models;
namespace MyApp.WebApi.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<User> Users => Set<User>();
}注册 DbContext 和添加迁移(略,可执行 dotnet ef migrations add Initial)。
在 Program.cs 中添加:
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
// ... 其他 using
var builder = WebApplication.CreateBuilder(args);
// 读取 JWT 配置
var jwtConfig = builder.Configuration.GetSection("Jwt");
var key = Encoding.UTF8.GetBytes(jwtConfig["Key"]!);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtConfig["Issuer"],
ValidateAudience = true,
ValidAudience = jwtConfig["Audience"],
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddAuthorization();
// 添加 Swagger 支持(略)// MyApp.WebApi/Controllers/AuthController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using MyApp.Shared.DTOs;
using MyApp.Shared.Responses;
using MyApp.WebApi.Data;
using MyApp.WebApi.Models;
using BCrypt.Net; // 需要安装 BCrypt.Net-Next
namespace MyApp.WebApi.Controllers;
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly AppDbContext _context;
private readonly IConfiguration _config;
public AuthController(AppDbContext context, IConfiguration config)
{
_context = context;
_config = config;
}
[HttpPost("login")]
public async Task<ActionResult<ApiResponse<LoginResponse>>> Login([FromBody] LoginRequest request)
{
var user = await _context.Users
.FirstOrDefaultAsync(u => u.UserName == request.UserName);
if (user == null || !BCrypt.Verify(request.Password, user.PasswordHash))
return Unauthorized(ApiResponse<LoginResponse>.Fail("用户名或密码错误", 401));
var token = GenerateJwtToken(user);
var response = new LoginResponse
{
Token = token,
User = new UserDto
{
Id = user.Id,
UserName = user.UserName,
Email = user.Email,
Avatar = user.Avatar
}
};
return Ok(ApiResponse<LoginResponse>.Ok(response));
}
private string GenerateJwtToken(User user)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.UserName)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(Convert.ToDouble(_config["Jwt:ExpireMinutes"])),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}注意:密码哈希使用 BCrypt,安装
BCrypt.Net-Next。
// MyApp.WebApi/Controllers/UsersController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MyApp.Shared.DTOs;
using MyApp.Shared.Responses;
using MyApp.WebApi.Data;
namespace MyApp.WebApi.Controllers;
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
private readonly AppDbContext _context;
public UsersController(AppDbContext context) => _context = context;
[HttpGet]
public async Task<ActionResult<ApiResponse<List<UserDto>>>> GetUsers()
{
var users = await _context.Users
.Select(u => new UserDto
{
Id = u.Id,
UserName = u.UserName,
Email = u.Email,
Avatar = u.Avatar
}).ToListAsync();
return Ok(ApiResponse<List<UserDto>>.Ok(users));
}
[HttpGet("{id}")]
public async Task<ActionResult<ApiResponse<UserDto>>> GetUser(int id)
{
var user = await _context.Users.FindAsync(id);
if (user == null)
return NotFound(ApiResponse<UserDto>.Fail("用户不存在", 404));
var dto = new UserDto { Id = user.Id, UserName = user.UserName, Email = user.Email, Avatar = user.Avatar };
return Ok(ApiResponse<UserDto>.Ok(dto));
}
}在 Program.cs 中:
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
});
// 之后
app.UseCors("AllowAll");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
// Swagger
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}采用 MVVM 模式,目录如下:
MyApp.Client/
├── App.xaml / App.xaml.cs
├── MainWindow.xaml / MainWindow.xaml.cs
├── Views/
│ ├── LoginView.xaml
│ └── MainView.xaml
├── ViewModels/
│ ├── LoginViewModel.cs
│ ├── MainViewModel.cs
│ └── (Base) ViewModelBase.cs
├── Services/
│ ├── IApiService.cs
│ ├── ApiService.cs
│ └── NavigationService.cs
├── Helpers/
│ └── RelayCommand.cs (或使用 CommunityToolkit 的 RelayCommand)
└── Assets/在 App.xaml.cs 中配置 DI 容器:
// App.xaml.cs
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MyApp.Client.Services;
using MyApp.Client.ViewModels;
using MyApp.Client.Views;
namespace MyApp.Client;
public partial class App : Application
{
private static IHost? _host;
public static IHost Host => _host ??= CreateHostBuilder().Build();
private static IHostBuilder CreateHostBuilder() =>
Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
// 注册 HttpClient
services.AddHttpClient<IApiService, ApiService>(client =>
{
client.BaseAddress = new Uri("https://localhost:5001"); // 应与服务端一致
});
// 注册 ViewModels
services.AddTransient<LoginViewModel>();
services.AddTransient<MainViewModel>();
// 注册 Views(用于导航)
services.AddTransient<LoginView>();
services.AddTransient<MainView>();
// 注册导航服务
services.AddSingleton<NavigationService>();
});
protected override async void OnStartup(StartupEventArgs e)
{
await Host.StartAsync();
var mainWindow = new MainWindow();
mainWindow.Show();
base.OnStartup(e);
}
protected override async void OnExit(ExitEventArgs e)
{
await Host.StopAsync();
_host?.Dispose();
base.OnExit(e);
}
}使用 CommunityToolkit.MVVM,我们可以用特性简化。但为了兼容性,我们手动实现 INotifyPropertyChanged 和 RelayCommand。
// ViewModels/ViewModelBase.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace MyApp.Client.ViewModels;
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
protected bool Set<T>(ref T field, T value, [CallerMemberName] string? name = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(name);
return true;
}
}// Helpers/RelayCommand.cs
using System.Windows.Input;
namespace MyApp.Client.Helpers;
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool>? _canExecute;
public RelayCommand(Action execute, Func<bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
public void Execute(object? parameter) => _execute();
}
public class RelayCommand<T> : ICommand
{
private readonly Action<T> _execute;
private readonly Func<T, bool>? _canExecute;
public RelayCommand(Action<T> execute, Func<T, bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public bool CanExecute(object? parameter) => _canExecute?.Invoke((T)parameter!) ?? true;
public void Execute(object? parameter) => _execute((T)parameter!);
}// Services/IApiService.cs
using MyApp.Shared.DTOs;
using MyApp.Shared.Responses;
namespace MyApp.Client.Services;
public interface IApiService
{
Task<ApiResponse<LoginResponse>> LoginAsync(LoginRequest request);
Task<ApiResponse<List<UserDto>>> GetUsersAsync();
Task<ApiResponse<UserDto>> GetUserAsync(int id);
}// Services/ApiService.cs
using System.Net.Http.Json;
using MyApp.Shared.DTOs;
using MyApp.Shared.Responses;
using System.Net;
namespace MyApp.Client.Services;
public class ApiService : IApiService
{
private readonly HttpClient _httpClient;
public ApiService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<ApiResponse<LoginResponse>> LoginAsync(LoginRequest request)
{
var response = await _httpClient.PostAsJsonAsync("/api/auth/login", request);
if (response.IsSuccessStatusCode)
return await response.Content.ReadFromJsonAsync<ApiResponse<LoginResponse>>()
?? ApiResponse<LoginResponse>.Fail("反序列化失败");
var error = await response.Content.ReadAsStringAsync();
return ApiResponse<LoginResponse>.Fail($"请求失败: {error}", (int)response.StatusCode);
}
public async Task<ApiResponse<List<UserDto>>> GetUsersAsync()
{
var response = await _httpClient.GetAsync("/api/users");
if (response.IsSuccessStatusCode)
return await response.Content.ReadFromJsonAsync<ApiResponse<List<UserDto>>>()
?? ApiResponse<List<UserDto>>.Fail("反序列化失败");
return ApiResponse<List<UserDto>>.Fail($"请求失败: {response.ReasonPhrase}", (int)response.StatusCode);
}
public async Task<ApiResponse<UserDto>> GetUserAsync(int id)
{
var response = await _httpClient.GetAsync($"/api/users/{id}");
if (response.IsSuccessStatusCode)
return await response.Content.ReadFromJsonAsync<ApiResponse<UserDto>>()
?? ApiResponse<UserDto>.Fail("反序列化失败");
return ApiResponse<UserDto>.Fail($"请求失败: {response.ReasonPhrase}", (int)response.StatusCode);
}
}// ViewModels/LoginViewModel.cs
using System.Threading.Tasks;
using System.Windows;
using MyApp.Client.Helpers;
using MyApp.Client.Services;
using MyApp.Shared.DTOs;
namespace MyApp.Client.ViewModels;
public class LoginViewModel : ViewModelBase
{
private readonly IApiService _apiService;
private readonly NavigationService _navigation;
private string _userName = string.Empty;
private string _password = string.Empty;
private bool _isLoading;
private string _errorMessage = string.Empty;
public string UserName { get => _userName; set => Set(ref _userName, value); }
public string Password { get => _password; set => Set(ref _password, value); }
public bool IsLoading { get => _isLoading; set => Set(ref _isLoading, value); }
public string ErrorMessage { get => _errorMessage; set => Set(ref _errorMessage, value); }
public RelayCommand LoginCommand { get; }
public LoginViewModel(IApiService apiService, NavigationService navigation)
{
_apiService = apiService;
_navigation = navigation;
LoginCommand = new RelayCommand(ExecuteLogin, CanLogin);
}
private bool CanLogin() => !IsLoading && !string.IsNullOrWhiteSpace(UserName) && !string.IsNullOrWhiteSpace(Password);
private async void ExecuteLogin()
{
IsLoading = true;
ErrorMessage = string.Empty;
try
{
var request = new LoginRequest { UserName = UserName, Password = Password };
var result = await _apiService.LoginAsync(request);
if (result.Success)
{
// 保存 Token(例如在静态属性中,或使用 SecureStorage)
App.Token = result.Data!.Token;
// 导航到主视图
_navigation.NavigateTo<MainView>();
}
else
{
ErrorMessage = result.Message ?? "登录失败";
}
}
catch (System.Exception ex)
{
ErrorMessage = $"网络错误: {ex.Message}";
}
finally
{
IsLoading = false;
LoginCommand.RaiseCanExecuteChanged(); // 手动刷新命令状态
}
}
}在 App 中添加静态 Token 属性:
public static string? Token { get; set; }并在 ApiService 的构造函数中注入 Token(或通过拦截器)。我们可以修改 ApiService 使其每次请求前附加 Token:
// 在 ApiService 中添加方法
private void SetAuthorizationHeader()
{
if (!string.IsNullOrEmpty(App.Token))
_httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", App.Token);
}并在每个请求前调用。
// ViewModels/MainViewModel.cs
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using MyApp.Client.Helpers;
using MyApp.Client.Services;
using MyApp.Shared.DTOs;
namespace MyApp.Client.ViewModels;
public class MainViewModel : ViewModelBase
{
private readonly IApiService _apiService;
private ObservableCollection<UserDto> _users = new();
private bool _isLoading;
private string _statusMessage = string.Empty;
public ObservableCollection<UserDto> Users
{
get => _users;
set => Set(ref _users, value);
}
public bool IsLoading { get => _isLoading; set => Set(ref _isLoading, value); }
public string StatusMessage { get => _statusMessage; set => Set(ref _statusMessage, value); }
public RelayCommand LoadUsersCommand { get; }
public MainViewModel(IApiService apiService)
{
_apiService = apiService;
LoadUsersCommand = new RelayCommand(ExecuteLoadUsers, CanLoadUsers);
}
private bool CanLoadUsers() => !IsLoading;
private async void ExecuteLoadUsers()
{
IsLoading = true;
StatusMessage = "加载中...";
Users.Clear();
try
{
var result = await _apiService.GetUsersAsync();
if (result.Success && result.Data != null)
{
foreach (var user in result.Data)
Users.Add(user);
StatusMessage = $"加载成功,共 {Users.Count} 条记录";
}
else
{
StatusMessage = result.Message ?? "加载失败";
}
}
catch (System.Exception ex)
{
StatusMessage = $"错误: {ex.Message}";
}
finally
{
IsLoading = false;
LoadUsersCommand.RaiseCanExecuteChanged();
}
}
}// Services/NavigationService.cs
using System;
using System.Windows;
using System.Windows.Controls;
namespace MyApp.Client.Services;
public class NavigationService
{
private readonly IServiceProvider _serviceProvider;
public NavigationService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void NavigateTo<TView>() where TView : Page
{
var view = _serviceProvider.GetService<TView>();
if (view == null)
throw new InvalidOperationException($"View {typeof(TView).Name} not registered.");
var mainWindow = Application.Current.MainWindow as MainWindow;
if (mainWindow?.MainFrame != null)
{
mainWindow.MainFrame.Navigate(view);
}
}
}MainWindow.xaml 作为容器,包含一个 Frame 用于导航:
<Window x:Class="MyApp.Client.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MyApp" Height="600" Width="800">
<Grid>
<Frame x:Name="MainFrame" NavigationUIVisibility="Hidden" />
</Grid>
</Window>在 MainWindow.xaml.cs 中,加载后导航到 LoginView:
public MainWindow()
{
InitializeComponent();
Loaded += (s, e) =>
{
var nav = App.Host.Services.GetRequiredService<NavigationService>();
nav.NavigateTo<LoginView>();
};
}LoginView.xaml(Page):
<Page x:Class="MyApp.Client.Views.LoginView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="LoginView">
<Grid>
<StackPanel Width="300" VerticalAlignment="Center">
<TextBlock Text="用户登录" FontSize="24" HorizontalAlignment="Center" Margin="0,0,0,20"/>
<TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}"
Margin="0,5" Padding="5" />
<PasswordBox x:Name="PasswordBox" Margin="0,5" Padding="5" />
<TextBlock Text="{Binding ErrorMessage}" Foreground="Red" Margin="0,5" />
<Button Content="登录" Command="{Binding LoginCommand}"
IsEnabled="{Binding IsLoading, Converter={StaticResource InverseBooleanConverter}}"
Margin="0,10" Padding="10"/>
<TextBlock Text="{Binding IsLoading, Converter={StaticResource BooleanToTextConverter}}"
HorizontalAlignment="Center" />
</StackPanel>
</Grid>
</Page>注意:PasswordBox 的密码绑定需要额外处理(可使用附加属性或通过代码在 ViewModel 中获取),此处为简化,可以在 View 的代码后置中获取密码并调用 ViewModel 方法。建议使用 MVVM 模式下的 PasswordBox 绑定方案(如使用
Interaction或附加属性)。
我们可以添加一个附加行为来绑定 Password:
// Helpers/PasswordBoxHelper.cs
public static class PasswordBoxHelper
{
public static readonly DependencyProperty BoundPasswordProperty =
DependencyProperty.RegisterAttached("BoundPassword", typeof(string), typeof(PasswordBoxHelper),
new PropertyMetadata(string.Empty, OnBoundPasswordChanged));
public static string GetBoundPassword(DependencyObject obj) => (string)obj.GetValue(BoundPasswordProperty);
public static void SetBoundPassword(DependencyObject obj, string value) => obj.SetValue(BoundPasswordProperty, value);
private static void OnBoundPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is PasswordBox pb)
{
pb.PasswordChanged -= PasswordChanged;
if (e.NewValue != null)
pb.Password = e.NewValue.ToString();
pb.PasswordChanged += PasswordChanged;
}
}
private static void PasswordChanged(object sender, RoutedEventArgs e)
{
if (sender is PasswordBox pb)
SetBoundPassword(pb, pb.Password);
}
}然后在 LoginView 中绑定 PasswordBox 的 BoundPassword 到 ViewModel 的 Password 属性。
MainView.xaml:
<Page x:Class="MyApp.Client.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainView">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0">
<Button Content="加载用户" Command="{Binding LoadUsersCommand}" Margin="5"/>
<TextBlock Text="{Binding StatusMessage}" Margin="10,5" VerticalAlignment="Center"/>
<Button Content="注销" Click="LogoutButton_Click" Margin="20,5,0,5" HorizontalAlignment="Right"/>
</StackPanel>
<ListView Grid.Row="1" ItemsSource="{Binding Users}" Margin="0,10">
<ListView.View>
<GridView>
<GridViewColumn Header="ID" DisplayMemberBinding="{Binding Id}" Width="50"/>
<GridViewColumn Header="用户名" DisplayMemberBinding="{Binding UserName}" Width="100"/>
<GridViewColumn Header="邮箱" DisplayMemberBinding="{Binding Email}" Width="200"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Page>在 ApiService 中,我们可以使用 Polly 或简单重试,但本文不展开。建议在生产环境中添加全局异常捕获和超时设置。
https://localhost:5001。App 初始化 DI,创建主窗口,导航到 LoginView。MainView。dotnet publish -c Release 生成发布文件。appsettings.json 中的连接字符串和 JWT Key,使用环境变量覆盖。ApiService 中的 BaseAddress 为服务端实际地址(可使用配置文件)。IMemoryCache 缓存用户数据。你可以参考以下 GitHub 仓库结构(本文完整代码已整理,可依据此搭建):
MyApp/
├── MyApp.Shared/
├── MyApp.WebApi/
├── MyApp.Client/
└── README.md通过本文,你已掌握使用 C# + WPF + WebApi 开发前后端分离桌面应用的全流程。这套架构不仅清晰、可维护,而且能轻松扩展为多端共享业务逻辑。无论是企业内部管理系统,还是面向消费者的工具软件,都能从中受益。如果你在实践中有任何问题,欢迎在评论区交流。
参考资源:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。