Pick Station WPF

Warehouse pick station application - reverse engineered

Reverse Engineering .NET WPF MVVM API Integration

Overview

A WPF application that replicates and extends functionality of a closed-source warehouse pick station system. This was created through reverse engineering the original binary to understand its API integrations and system behavior.

Key Features

Live Product Details via REST API

A new feature that fetches product details in real-time from the REST API, eliminating the need for a local database that only synced every 24 hours. Now pick list items show current product information instantly.

private async Task<List<Product>?> GetProductDetails(List<PickListItem> pickListItems)
{
    var barcodes = pickListItems
        .Select(p => p.PdBarcode)
        .Where(b => !string.IsNullOrEmpty(b))
        .ToList();

    return await _apiService.GetProductDetailsAsync(barcodes, SelectedLocationProfile);
}

public async Task<List<Product>?> GetProductDetailsAsync(List<string> skus, LocationProfile profile)
{
    var parameters = new List<KeyValuePair<string, string?>>
    {
        new("skulist", string.Join(",", skus)),
        new("service", configService.RestApi.Service),
        new("username", configService.RestApi.Username),
        new("password", configService.RestApi.Password)
    };

    var url = $"{configService.RestApiUrl}?{await new FormUrlEncodedContent(parameters).ReadAsStringAsync()}";
    var response = await HttpClient.GetAsync(url);
    var json = await response.Content.ReadAsStringAsync();
    var productResponse = JsonConvert.DeserializeObject<ProductDetailResponse>(json);

    if (productResponse?.ServiceResult?.Success == true)
        return productResponse.ProductDetails;

    return null;
}

SOAP API Integration

The application communicates with the backend using SOAP XML requests. This snippet shows how the pick list is fetched using a SOAP envelope with authentication.

public async Task<List<PickListItem>?> GetPickListWaitingAsync(LocationProfile profile)
{
    if (configService.SoapApiUrl == null) return new List<PickListItem>();

    var soapXml = $"""
     <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tem="http://tempuri.org/">
                        <soapenv:Header/>
                        <soapenv:Body>
                           <tem:GetpickListWaiting>
                              <tem:locationSource>{profile.LocationSourceId}</tem:locationSource>
                              <tem:username>{configService.SoapApi?.Username}</tem:username>
                              <tem:password>{configService.SoapApi?.Password}</tem:password>
                           </tem:GetpickListWaiting>
                        </soapenv:Body>
                     </soapenv:Envelope>
     """;

    var request = new HttpRequestMessage(HttpMethod.Post, configService.SoapApiUrl);
    request.Headers.Add("SOAPAction", "http://yourservice.com/IPickService/GetpickListWaiting");
    request.Content = new StringContent(soapXml, Encoding.UTF8, "text/xml");

    var response = await HttpClient.SendAsync(request);
    var responseXml = await response.Content.ReadAsStringAsync();
    
    var xdoc = XDocument.Parse(responseXml);
    // Parse XML response into PickListItem objects
}

Drag and Drop Window Positioning

The SideTab window can be dragged and positioned anywhere on screen. Uses mouse capture and position tracking to enable smooth dragging behavior.

public partial class SideTabWindow : Window
{
    private bool _isDragging;
    private Point _startPoint;

    private void Border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        _startPoint = e.GetPosition(this);
        (sender as FrameworkElement)?.CaptureMouse();
        _isDragging = false;
    }

    private void Border_MouseMove(object sender, MouseEventArgs e)
    {
        if ((sender as FrameworkElement)?.IsMouseCaptured == true && e.LeftButton == MouseButtonState.Pressed)
        {
            var currentPoint = e.GetPosition(this);
            var diff = _startPoint - currentPoint;

            if (Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance || _isDragging)
            {
                _isDragging = true;
                var screenPos = PointToScreen(currentPoint);
                Top = screenPos.Y - _startPoint.Y;
            }
        }
    }

    private void Border_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        (sender as FrameworkElement)?.ReleaseMouseCapture();
        if (!_isDragging)
            if (DataContext is SideTabViewModel viewModel)
                viewModel.OpenMainWindowCommand.Execute(null);
    }
}

Serial Port Printing with Fallback

Direct communication with thermal receipt printers via serial port using ESC/POS-like commands. If the serial printer fails or is unavailable, it gracefully falls back to the standard Windows Print Dialog.

public void PrintReceipt(List<PrintablePickCard> items, string printerPort, string stationBranch)
{
    bool portAvailable = SerialPort.GetPortNames().Contains(printerPort);
    if (!portAvailable) return;

    try
    {
        using var serialPort = new SerialPort(printerPort, 9600, Parity.None, 8, StopBits.One);
        serialPort.Encoding = Encoding.ASCII;
        serialPort.Open();

        // Bold ON
        serialPort.Write(new byte[] { 0x1B, 0x21, 0x08 }, 0, 3);
        serialPort.Write($"PICK LIST ({stationBranch})\n");
        
        serialPort.Write(new byte[] { 0x1D, 0x56, 0x00 }, 0, 3); // Cut paper
        serialPort.Close();
    }
    catch (Exception ex)
    {
        // Fallback to Windows Print Dialog if serial printing fails
        PrintDocument(items, stationBranch, filterConnector, filterOrderId, filterBarcode);
    }
}

// Fallback: Standard Windows Print Dialog
public void PrintDocument(List<PrintablePickCard> items, string stationBranch, ...)
{
    var printDialog = new PrintDialog();
    if (printDialog.ShowDialog() == true)
    {
        var flowDocument = new FlowDocument();
        
        var header = new Paragraph();
        header.FontSize = 16;
        header.TextAlignment = TextAlignment.Center;
        header.Inlines.Add($"PICK LIST ({stationBranch})\r\n");
        header.Inlines.Add($"Printed {DateTime.Now:dd/MM/yyyy hh:mm}");
        flowDocument.Blocks.Add(header);

        foreach (var item in items)
        {
            var itemParagraph = new Paragraph();
            itemParagraph.FontSize = 13;
            itemParagraph.Inlines.Add($"\r\nOrder: {item.WebOrderNumber}\r\n");
            itemParagraph.Inlines.Add($"Product: {item.Brand} - {item.ProductName}\r\n");
            itemParagraph.Inlines.Add($"Barcode: {item.Barcode}\r\n");
            flowDocument.Blocks.Add(itemParagraph);
        }

        printDialog.PrintDocument(flowDocument.DocumentPaginator, "PICK LIST");
    }
}

Persistent Barcode State

Scanned barcodes are persisted to a JSON file in the local app data folder, allowing the application to maintain state across restarts.

public class ScannedBarcodeService
{
    private readonly string _filePath;

    public ScannedBarcodeService()
    {
        var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        var appFolder = Path.Combine(appDataPath, "PickStationWPF");
        Directory.CreateDirectory(appFolder);
        _filePath = Path.Combine(appFolder, "scannedBarcodes.json");
    }

    public HashSet<string> LoadScannedBarcodes()
    {
        if (!File.Exists(_filePath)) return [];
        var json = File.ReadAllText(_filePath);
        var barcodes = JsonConvert.DeserializeObject<List<string>>(json);
        return [..barcodes ?? []];
    }

    public void SaveScannedBarcodes(HashSet<string> barcodes)
    {
        var json = JsonConvert.SerializeObject(barcodes.ToList(), Formatting.Indented);
        File.WriteAllText(_filePath, json);
    }
}

Challenges & Solutions