首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >使用 C# + WPF + WebApi 构建前后端分离的桌面应用——从零到部署

使用 C# + WPF + WebApi 构建前后端分离的桌面应用——从零到部署

原创
作者头像
用户12339161
发布2026-07-29 10:41:55
发布2026-07-29 10:41:55
2840
举报

使用 C# + WPF + WebApi 构建前后端分离的桌面应用——从零到部署

在现代桌面应用开发中,前后端分离早已不是 Web 专属。WPF 作为成熟的 UI 框架,搭配 ASP.NET Core WebApi 作为服务端,既能发挥桌面端丰富的交互能力,又能利用 WebApi 实现业务逻辑统一、多客户端复用。本文将带你从零搭建一套完整的“WPF 客户端 + WebApi 服务端”应用程序,涵盖项目初始化、接口开发、数据库集成、JWT 认证、WPF MVVM 架构、HttpClient 调用、异步命令、错误处理及最终部署,所有代码均基于 .NET 8/9,适合中级开发者实战参考。


一、为什么选择这套技术栈?

  • C#:类型安全、性能优异,全平台支持(.NET Core 后)。
  • WPF:成熟的 XAML UI 框架,数据绑定、样式、模板强大,适合复杂桌面应用。
  • WebApi:RESTful 接口,跨平台、轻量,易于维护和扩展。
  • 前后端分离:便于团队协作,服务端可同时服务于 Web、移动端、桌面端。
  • 生态完善:EF Core、JWT、Swagger、HttpClientFactory、Prism(可选)等周边丰富。

本文不依赖第三方 MVVM 框架(如 Prism),而是使用 .NET 内置的 CommunityToolkit.MVVM 来简化开发,但也会提到如何替换为 Prism。


二、整体解决方案结构

我们创建一个 Visual Studio 解决方案,包含三个项目:

代码语言:javascript
复制
Solution: MyApp
├── MyApp.WebApi          (ASP.NET Core WebApi 项目)
├── MyApp.Client          (WPF 应用程序)
└── MyApp.Shared          (类库,存放 DTO、枚举、常量等共享类型)

使用 dotnet CLI 或 VS 创建:

代码语言:javascript
复制
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.Shared
  • MyApp.Client 引用 MyApp.Shared
  • MyApp.Client 添加包:CommunityToolkit.MVVMMicrosoft.Extensions.HttpMicrosoft.Extensions.DependencyInjection

三、共享层(MyApp.Shared)—— 前后端契约

MyApp.Shared 中定义 DTO 和枚举,确保服务端和客户端使用相同的类型。

3.1 用户 DTO

代码语言:javascript
复制
// 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!;
}

3.2 统一响应结构

代码语言:javascript
复制
// 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 };
}

3.3 常量

代码语言:javascript
复制
// 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";
}

四、服务端(MyApp.WebApi)—— 搭建 WebApi

4.1 项目配置

修改 appsettings.json

代码语言:javascript
复制
{
  "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": "*"
}

4.2 安装 NuGet 包

代码语言:javascript
复制
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

4.3 数据层(EF Core + SQL Server)

创建实体和 DbContext:

代码语言:javascript
复制
// 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;
}
代码语言:javascript
复制
// 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)。

4.4 JWT 认证配置

Program.cs 中添加:

代码语言:javascript
复制
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 支持(略)

4.5 实现 Auth 控制器

代码语言:javascript
复制
// 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

4.6 用户管理控制器(带授权)

代码语言:javascript
复制
// 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));
    }
}

4.7 启用 Swagger 和 CORS

Program.cs 中:

代码语言:javascript
复制
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();
}

五、客户端(MyApp.Client)—— WPF + MVVM

5.1 项目结构

采用 MVVM 模式,目录如下:

代码语言:javascript
复制
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/

5.2 依赖注入和 HttpClient 配置

App.xaml.cs 中配置 DI 容器:

代码语言:javascript
复制
// 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);
    }
}

5.3 基础 ViewModel 和命令

使用 CommunityToolkit.MVVM,我们可以用特性简化。但为了兼容性,我们手动实现 INotifyPropertyChangedRelayCommand

代码语言:javascript
复制
// 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;
    }
}
代码语言:javascript
复制
// 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!);
}

5.4 服务层:调用 WebApi

代码语言:javascript
复制
// 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);
}
代码语言:javascript
复制
// 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);
    }
}

5.5 登录 ViewModel

代码语言:javascript
复制
// 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 属性:

代码语言:javascript
复制
public static string? Token { get; set; }

并在 ApiService 的构造函数中注入 Token(或通过拦截器)。我们可以修改 ApiService 使其每次请求前附加 Token:

代码语言:javascript
复制
// 在 ApiService 中添加方法
private void SetAuthorizationHeader()
{
    if (!string.IsNullOrEmpty(App.Token))
        _httpClient.DefaultRequestHeaders.Authorization = 
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", App.Token);
}

并在每个请求前调用。

5.6 主 ViewModel

代码语言:javascript
复制
// 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();
        }
    }
}

5.7 导航服务

代码语言:javascript
复制
// 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);
        }
    }
}

5.8 视图(XAML)

MainWindow.xaml 作为容器,包含一个 Frame 用于导航:

代码语言:javascript
复制
<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

代码语言:javascript
复制
public MainWindow()
{
    InitializeComponent();
    Loaded += (s, e) =>
    {
        var nav = App.Host.Services.GetRequiredService<NavigationService>();
        nav.NavigateTo<LoginView>();
    };
}

LoginView.xaml(Page):

代码语言:javascript
复制
<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:

代码语言:javascript
复制
// 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 中绑定 PasswordBoxBoundPassword 到 ViewModel 的 Password 属性。

MainView.xaml

代码语言:javascript
复制
<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>

5.9 错误处理和超时

ApiService 中,我们可以使用 Polly 或简单重试,但本文不展开。建议在生产环境中添加全局异常捕获和超时设置。


六、应用启动流程

  1. 服务端启动(dotnet run),默认监听 https://localhost:5001
  2. 客户端启动,App 初始化 DI,创建主窗口,导航到 LoginView
  3. 用户输入凭据,点击登录,调用 WebApi 认证接口。
  4. 认证成功,保存 Token,导航到 MainView
  5. 主视图加载用户列表(需携带 Token)。

七、部署注意事项

7.1 服务端部署

  • 使用 dotnet publish -c Release 生成发布文件。
  • 部署到 IIS、Azure App Service 或 Linux 容器。
  • 修改 appsettings.json 中的连接字符串和 JWT Key,使用环境变量覆盖。

7.2 客户端部署

  • WPF 应用可发布为 ClickOnce、MSIX 或简单的 xcopy 部署。
  • 修改 ApiService 中的 BaseAddress 为服务端实际地址(可使用配置文件)。

八、扩展与优化建议

  • 使用 Prism:如果项目复杂,可引入 Prism 实现模块化、导航和事件聚合。
  • 缓存:在客户端使用 IMemoryCache 缓存用户数据。
  • 信号通知:集成 SignalR 实现实时推送。
  • 日志:使用 Serilog 记录客户端和服务端日志。
  • 单元测试:为 ViewModel 和服务层编写 xUnit 测试。

九、完整代码仓库示例

你可以参考以下 GitHub 仓库结构(本文完整代码已整理,可依据此搭建):

代码语言:javascript
复制
MyApp/
├── MyApp.Shared/
├── MyApp.WebApi/
├── MyApp.Client/
└── README.md

十、结语

通过本文,你已掌握使用 C# + WPF + WebApi 开发前后端分离桌面应用的全流程。这套架构不仅清晰、可维护,而且能轻松扩展为多端共享业务逻辑。无论是企业内部管理系统,还是面向消费者的工具软件,都能从中受益。如果你在实践中有任何问题,欢迎在评论区交流。

参考资源

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 使用 C# + WPF + WebApi 构建前后端分离的桌面应用——从零到部署
    • 一、为什么选择这套技术栈?
    • 二、整体解决方案结构
    • 三、共享层(MyApp.Shared)—— 前后端契约
      • 3.1 用户 DTO
      • 3.2 统一响应结构
      • 3.3 常量
    • 四、服务端(MyApp.WebApi)—— 搭建 WebApi
      • 4.1 项目配置
      • 4.2 安装 NuGet 包
      • 4.3 数据层(EF Core + SQL Server)
      • 4.4 JWT 认证配置
      • 4.5 实现 Auth 控制器
      • 4.6 用户管理控制器(带授权)
      • 4.7 启用 Swagger 和 CORS
    • 五、客户端(MyApp.Client)—— WPF + MVVM
      • 5.1 项目结构
      • 5.2 依赖注入和 HttpClient 配置
      • 5.3 基础 ViewModel 和命令
      • 5.4 服务层:调用 WebApi
      • 5.5 登录 ViewModel
      • 5.6 主 ViewModel
      • 5.7 导航服务
      • 5.8 视图(XAML)
      • 5.9 错误处理和超时
    • 六、应用启动流程
    • 七、部署注意事项
      • 7.1 服务端部署
      • 7.2 客户端部署
    • 八、扩展与优化建议
    • 九、完整代码仓库示例
    • 十、结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档