Pick Station WPF
Warehouse pick station application - reverse engineered
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.
- Modern WPF application using MVVM pattern with CommunityToolkit.Mvvm
- Integrates with SOAP and REST APIs for pick list management
- Serial port communication for receipt printing
- Multi-monitor support with configurable display options
- Barcode scanning support with persistent state
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
- Reverse engineering the API: Used Visual Studio's .NET Peek feature to decompile and understand the original binary's API integrations and message formats
- Serial port communication: Found the printer uses ESC/POS-compatible commands, allowing direct communication without vendor drivers
- Multi-monitor support: Implemented using WPF's System.Windows.Forms.Screen API to detect and position windows on secondary displays
- Modernizing legacy code: Rebuilt using modern .NET and WPF patterns (MVVM, CommunityToolkit) while maintaining compatibility with the original API