Minh’s project started differently from mine. I had a .NET Framework application that needed to run on modern infrastructure. He had a WPF desktop application that needed to become a website.

Same AI tools, same small team, fundamentally different problem.

Desktop-to-web doesn’t feel like a migration. It feels like a rewrite with the original source code as reference material. The UI is entirely incompatible. The desktop UI paradigm (rich client, local state, Windows controls) is foreign to the web paradigm (HTTP stateless, browser API, component lifecycle). And yet — with AI as the bridge — Minh’s team moved faster than anyone expected.

This post covers the WPF → React/Next.js migration. What AI can translate. What it cannot. And the architecture decisions that made the difference between a messy port and a clean modern web application.

The First Decision: Don’t Start with UI

Every instinct says to start with the UI. The user sees the UI. The boss demos the UI. The client approves the UI.

Don’t start with UI.

WPF UI code is deeply coupled to Windows, to XAML, to MVVM patterns that have no direct equivalent in the web world. If you start by trying to convert XAML to JSX, you’ll spend all your time fighting the wrong battle, and you’ll port the architecture problems that made the WPF app hard to maintain into your new web app.

Start with the business logic. Here’s why: the business logic in a WPF application is usually the most valuable thing in the codebase. If you’re building an order management system, the rules for calculating order totals, applying discounts, managing inventory, and generating reports — that’s the asset. The WPF UI is just the delivery mechanism. And delivery mechanisms can be replaced.

The extraction sequence:

  1. Map your layers: Where is pure business logic? Where does it bleed into UI? Where is data access?
  2. Extract business logic into a .NET API: Build a modern ASP.NET Core API (.NET 10) that exposes the business logic via HTTP endpoints
  3. Extract data access: Move to an ORM (EF Core or Dapper) if you were using raw ADO.NET
  4. Then, and only then, build the React frontend: Consuming the API you just built

This approach means your WPF app and your new web app can run simultaneously during migration. The WPF app can even be updated to consume the new API (using WebView2 or HTTP calls) while the React frontend is being built.

WPF Migration Decision Tree

Assessing the WPF Codebase with AI

Before Minh wrote a single line of React, he spent two weeks on assessment. Here’s the AI-assisted process:

Prompt for WPF architecture analysis:

I'm analyzing a WPF application before migrating it to a web application 
(React + Next.js + .NET 10 API backend).

Here is a ViewModel or code-behind file:
[paste file]

Please analyze and tell me:
1. What does this screen/feature DO in business terms?
2. What business logic is embedded here that should be in an API?
3. What data does this screen read from / write to?
4. What Windows-specific features does it use? (File dialogs, clipboard, 
   hardware, notifications, registry, etc.)
5. What MVVM patterns are used and what's the React equivalent?
6. Migration complexity: Simple (pure UI mapping) / Medium (logic extraction 
   needed) / Hard (Windows-specific features with no web equivalent)

Minh ran this analysis on every ViewModel in the application over two days. The output was a migration map:

ScreenBusiness LogicWindows-SpecificWeb EquivalentComplexity
OrderEntryHigh (validation)File drag-and-dropFile upload APIMedium
ReportsLow (display only)Crystal ReportsPDF generation APISimple
PrintQueueNoneWindows print spoolerBrowser print APIHard
DashboardNone (display)NoneDirect portSimple
SettingsLow (save prefs)RegistryAPI + localStorageSimple

The PrintQueue screen was the red flag. It communicated directly with the Windows print spooler — something a web browser fundamentally cannot do. This was flagged as “redesign required,” not “migration required.” The web version would use browser print API (window.print()) for simple cases and a backend PDF service for complex reports.

Finding this early saved weeks of wasted effort trying to “migrate” something that was architecturally unmigrate-able.

Extracting Business Logic to a .NET 10 API

The backend extraction is, frankly, similar to the .NET Framework migration covered in Part 3. The difference: you’re not trying to preserve the WPF interaction model. You’re building a clean REST API.

The target architecture:

Target Clean Architecture

If you have a WPF ViewModel that does this:

// WPF OrderEntryViewModel.cs
public class OrderEntryViewModel : ViewModelBase
{
    private readonly IOrderRepository _repo;
    
    public async Task SaveOrderAsync()
    {
        // Business logic mixed with UI concerns
        if (string.IsNullOrEmpty(CustomerName))
        {
            ErrorMessage = "Customer name is required"; // UI concern
            return;
        }
        
        if (OrderLines.Count == 0)
        {
            ErrorMessage = "Add at least one item"; // UI concern
            return;
        }
        
        // This is the real business logic:
        var order = Order.Create(
            CustomerName, 
            OrderLines.Select(l => new OrderLine(l.ProductId, l.Quantity, l.Price)).ToList()
        );
        
        await _repo.SaveAsync(order);
        NavigateTo(nameof(OrderConfirmationView)); // UI concern
    }
}

The extracted API endpoint looks like this:

// .NET 10 Minimal API
app.MapPost("/api/orders", async (
    CreateOrderRequest request,
    ISender sender,
    CancellationToken ct) =>
{
    var result = await sender.Send(new CreateOrderCommand(
        request.CustomerName,
        request.Lines.Select(l => new OrderLineDto(l.ProductId, l.Quantity, l.Price)).ToList()
    ), ct);
    
    return result.IsSuccess 
        ? Results.Created($"/api/orders/{result.Value.OrderId}", result.Value)
        : Results.ValidationProblem(result.Errors.ToDictionary());
})
.WithName("CreateOrder")
.Produces<OrderCreatedResponse>(201)
.ProducesValidationProblem();

The validation business rules go in a FluentValidation validator, not in the endpoint. The UI concerns (error messages, navigation) go in the React frontend. The business logic is now in the Application layer command handler.

AI prompt for ViewModel extraction:

I'm extracting business logic from this WPF ViewModel to a .NET 10 API 
using Clean Architecture with CQRS (MediatR + FluentValidation).

Architecture:
- Command/Query via MediatR
- Validation via FluentValidation
- Return type: Result<T> (not throwing exceptions)
- Target framework: .NET 10 Minimal API

Here is the ViewModel:
[paste ViewModel]

Please:
1. Identify the pure business logic (separate from UI and navigation)
2. Create the CreateXCommand, handler, and FluentValidation validator
3. Create the Minimal API endpoint that uses this command
4. Identify what goes in React (error display, navigation, form state)
5. Flag anything that's a Windows-specific concern needing special handling

The React/Next.js Frontend

Once the API is built and tested, the React frontend is much more straightforward. You’re building a new web application against a clean API, not trying to translate XAML pixel-by-pixel.

Technology choices we made:

  • Next.js App Router — for server-side rendering, API routes, and file-based routing
  • React Hook Form — for form management (replaces WPF data binding)
  • Zod — for client-side schema validation (parallel to backend FluentValidation)
  • TanStack Query (React Query) — for data fetching and caching (replaces WPF commands + repositories)
  • Radix UI / shadcn/ui — for accessible component primitives (replaces WPF controls)
  • Tailwind CSS — for styling

The MVVM to React mental model shift:

WPF ConceptReact Equivalent
ObservableCollection<T>useState<T[]>() or TanStack Query data
INotifyPropertyChangedReact component state + useState
RelayCommand / ICommandEvent handlers + async functions
Data binding ({Binding})Controlled components + value={state}
DataContextProps + Context API / Zustand
ViewModel constructor injectionCustom hooks
NavigationServiceNext.js router
Code-behind event handlersComponent event props

AI prompt for MVVM → React conversion:

I'm converting a WPF ViewModel + View pair to a React component for 
Next.js App Router.

The app uses: React Hook Form, Zod for validation, TanStack Query for 
data fetching, Tailwind CSS for styling, and calls a .NET 10 REST API.

Here is the WPF ViewModel:
[paste ViewModel]

Here is the XAML View:
[paste XAML]

Here is the API contract (endpoint + request/response DTOs):
[paste API definition]

Please create:
1. A React component (.tsx) for this screen
2. A custom hook that handles data fetching via TanStack Query
3. A Zod schema matching the form validation
4. Use React Hook Form for form state management
5. Handle loading, error, and success states

Style with Tailwind. Don't create custom CSS.
Keep business logic in the API — this component only handles UI concerns.

The output quality from AI here is noticeably better than legacy .NET migration, because React/Next.js patterns are well-represented in AI training data. The AI knows how to structure a useQuery, how to handle form submission with React Hook Form, and how to map Zod validation to inline errors. The main thing you’re checking in review is whether the API calls are correct and whether the UX matches the expected behavior.

Handling WPF-Specific Features on the Web

File System Access

WPF: OpenFileDialog, direct File.ReadAllBytes()
Web: <input type="file">, File API, upload to API endpoint

// React file upload
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
  const file = e.target.files?.[0];
  if (!file) return;
  
  const formData = new FormData();
  formData.append('file', file);
  
  const response = await fetch('/api/documents/upload', {
    method: 'POST',
    body: formData,
  });
};

Desktop Notifications

WPF: TaskbarItemInfo, System.Windows.Forms.NotifyIcon
Web: Web Notifications API (browser permission required)

const requestNotificationPermission = async () => {
  if (Notification.permission !== 'granted') {
    await Notification.requestPermission();
  }
};

const showNotification = (title: string, body: string) => {
  if (Notification.permission === 'granted') {
    new Notification(title, { body });
  }
};

Keyboard Shortcuts

WPF: InputBindings, KeyBinding
Web: Event listeners, React keyboard hooks

// using react-hotkeys-hook
import { useHotkeys } from 'react-hotkeys-hook';

useHotkeys('ctrl+s', () => handleSave(), { preventDefault: true });
useHotkeys('ctrl+z', () => handleUndo(), { preventDefault: true });

Printing

WPF: Windows print spooler, complex print layouts
Web: Browser print API for simple, PDF generation API for complex

// Simple case: browser print
const handlePrint = () => window.print();

// Complex case: fetch PDF from API
const handlePrintReport = async () => {
  const pdf = await fetch(`/api/reports/${reportId}/pdf`);
  const blob = await pdf.blob();
  const url = URL.createObjectURL(blob);
  window.open(url, '_blank');
};

The Strangler Fig Migration Strategy

Minh’s team didn’t switch off WPF and switch on React simultaneously. They used the Strangler Fig pattern — gradually replacing parts of the WPF application with web equivalents while both systems run in parallel.

Phase structure:

PhaseWhat runsUser experience
1: API extractionWPF + .NET APIWPF app, internal refactor only
2: HybridWPF (most screens) + WebView2 (new screens)Mostly unchanged
3: Web-firstNext.js (main) + WPF (legacy screens still pending)Web UI for new features
4: CompleteNext.js onlyFull web experience

WebView2 (Microsoft’s embedded Chromium for Windows apps) was the bridge. Minh’s team embedded finished React screens into the WPF shell using WebView2. Users didn’t even notice the transition — they were still clicking within the same application window, but some screens were now React components running in an embedded browser.

Strangler Fig Migration Phases

// WPF code-behind: embed a web screen
private void ShowOrderEntry_Click(object sender, RoutedEventArgs e)
{
    // Old: navigate to WPF screen
    // NavigateTo(typeof(OrderEntryView));
    
    // New: open React screen in WebView2
    webView.Source = new Uri("https://app.company.com/orders/new");
}

This approach let the team ship value incrementally, get user feedback on the web interface before the full cutover, and avoid the big-bang risk of switching everything at once.

What We’d Tell Minh to Do Differently

Looking back, three things would have accelerated Minh’s migration:

1. Component library first

Before migrating any screen, build your shared component library: buttons, inputs, modals, tables, forms. Define the design system. Every migrated screen then draws from this library. Without it, Minh’s team built slightly different versions of the same dropdown component on five different screens.

2. API contract documentation from the start

The handoff between the backend (.NET API) and frontend (React) team was smoother when they had explicit API contracts (OpenAPI/Swagger). When the backend changed a response shape, the frontend found out two days later when React broke. Treat the API contract as a living specification that both sides own.

3. E2E tests capture WPF behavior before migration starts

Playwright scripts running against the WPF application (or against well-documented user workflows) gave us a baseline for “does the web version behave the same?” You can’t automate WPF UI testing at scale, but you can document the key user journeys and then automate those same journeys in Playwright against the web app.


This is Part 4 of a 7-part series: The AI-Powered Migration Playbook.

Series outline:

  1. Why AI Changes Everything (Part 1)
  2. The AI Migration Workflow (Part 2)
  3. .NET Framework 4.6 → .NET 10 with AI (Part 3)
  4. WPF to Web with React/Next.js (this post)
  5. The Human Side (Part 5)
  6. Measuring Success (Part 6)
  7. Lessons Learned (Part 7)
Export for reading

Comments