WinForms and WPF to Avalonia With the Same sgcUI API

sgcUI ships native controls for WinForms, WPF and Avalonia that share one model per product. The grid columns, the chart, the planner data and the theme are written once and compile unchanged for every host, so moving a screen to Avalonia, and with it to macOS and Linux, means changing the window code and the control types, not the screen logic. This kit shows how, with code from a sample that compiles and passes its self test on all three hosts.

A Windows Desktop App That Must Run on macOS and Linux

Line of business screens with grids, charts and schedulers, a data layer you want to keep, and no appetite for rewriting the screen logic in a new UI framework.

From WinForms

WinForms runs on Windows only. With sgcUI the TsgcWinForms* controls you use today have TsgcAvalonia* twins with the same members. The configuration code moves to a shared class library, the form becomes an Avalonia window, and DataSource becomes ItemsSource.

From WPF

WPF runs on Windows only too. Its sgcUI controls are the closest match to Avalonia: both bind with ItemsSource, both are declared in XAML with the same sgc namespace URI, and the UI thread call is the main code change.

The usual route for an existing WPF application is Avalonia XPF, which its vendor describes as "a binary-compatible WPF runtime" and "a fork of WPF that maintains API and binary compatibility across the managed surface area". At the time of writing (2026-10-20) XPF is sold as perpetual licences per application with twelve months of updates and support: Internal at EUR 9,500 per app (deployment to your own employees only), Business at EUR 29,500 per app (commercial deployment to macOS and desktop Linux, embedded Linux a EUR 21,250 add-on), and Enterprise from EUR 124,500 per app for mobile or the browser. Third party WPF suites can run on XPF with conditions: the XPF third party libraries notes say Win32 API shims are "required for DevExpress controls", that some DevExpress controls depend on System.Drawing.Common (GDI+), "which is deprecated on non-Windows platforms", that DevExpress controls on Linux require libgdiplus, and that "Issues most commonly arise with controls that use GDI or WinForms components". Avalonia itself is a free, MIT licensed UI framework. sgcUI for .NET is licensed per developer, from €380 for the .NET Standard edition, with full C# source code for WinForms, WPF and Avalonia, free binary redistribution and no per application fee. Free Community packages are on NuGet.

One Model, Three Thin Hosts

Each sgcUI product has a model that knows nothing about the host. The WinForms, WPF and Avalonia controls are thin views over it.

Shared models

TsgcGridColumns, TsgcGridData, TsgcChart, TsgcPlannerData, TsgcPlannerSchedulerOptions, TsgcPivotEngine and TsgcDiagramDocument are the same types on the three hosts. Code written against them compiles unchanged for each one.

The same member names

Columns, Data, ShowFooter, GroupSummaries, GroupBy, SaveLayoutToString, LoadLayoutFromString, Chart, Options, View, Date, PrintSettings, CreatePrintSource, Print, ShowPrintPreview and ExportToPDF, plus the grid events such as OnCellClick, OnSelectionChange and OnSortChange.

The same saved layout

A grid layout saved with SaveLayoutToString is the same JSON text on every host. In the sample the three hosts write their layout to a file and the three files have one SHA-256. Themes (TsgcUITheme, sgcUIApplication.Theme, sgcUIApplication.AccentColor, high contrast) are shared as well.

The Host Control of Every Product

The rule of thumb: replace the TsgcWinForms or TsgcWpf prefix by TsgcAvalonia. The exceptions are marked in the table.

Product WinForms WPF Avalonia
sgcGridTsgcWinFormsGridTsgcWpfGridTsgcAvaloniaGrid
sgcGrid declared columnin code, Columns.AddTsgcWpfGridColumn (XAML)TsgcAvaloniaGridColumn (XAML)
sgcChartsTsgcWinFormsChart, TsgcWinFormsChartEditorTsgcWpfChart, TsgcWpfChartEditorTsgcAvaloniaChart, TsgcAvaloniaChartEditor
sgcPivotGridTsgcWinFormsPivotGrid, TsgcWinFormsPivotFieldListTsgcWpfPivotGrid, TsgcWpfPivotFieldListTsgcAvaloniaPivotGrid, TsgcAvaloniaPivotFieldList
sgcPlannerTsgcWinFormsScheduler, TsgcWinFormsGantt, TsgcWinFormsKanbanTsgcWpfScheduler, TsgcWpfGantt, TsgcWpfKanbanTsgcAvaloniaScheduler, TsgcAvaloniaGantt, TsgcAvaloniaKanban
sgcDiagramTsgcWinFormsDiagramView, TsgcWinFormsDiagramPalette, TsgcWinFormsDiagramMinimapTsgcWpfDiagramView, TsgcWpfDiagramPalette, TsgcWpfDiagramMinimapTsgcAvaloniaDiagramView, TsgcAvaloniaDiagramPalette, TsgcAvaloniaDiagramMinimap
sgcGaugesTsgcWinForms<Kind>GaugeTsgcWpf<Kind>GaugeTsgcAvalonia<Kind>Gauge
Kind is Radial, Linear, Digital, Compass, Clock, LED, LEDBar, ProgressRing, VUMeter or Annunciator.
sgcDashboardTsgcWinFormsDashboardTsgcWpfDashboardTsgcAvaloniaDashboard
sgcDashboard tilesTsgcWinFormsDashboardTile, ...ChartTile, ...GridTile, ...GaugeTile, ...PivotTile, TsgcWinFormsKPICard, and TsgcWinFormsDashboardMapTileTsgcWpfDashboardTile, ...ChartTile, ...GridTile, ...GaugeTile, ...PivotTile, TsgcWpfKPICard. No map tile.TsgcAvaloniaDashboardTile, ...ChartTile, ...GridTile, ...GaugeTile, ...PivotTile, TsgcAvaloniaKPICard. No map tile.
sgcMapsSgcMapControl (namespace esegece.sgcMaps.WinForms)TsgcWpfMapTsgcAvaloniaMap
sgcEditorsTsgcWinForms<Editor>TsgcWpf<Editor>TsgcAvalonia<Editor>
Editor is ButtonEdit, CurrencyEdit, MaskEdit, SpinEdit, DatePicker, DateTimePicker, TimePicker, LookupEdit, SearchEdit, TokenEdit, Memo, ColorPicker, MonthCalendar, RadioGroup, CheckGroup, Rating or RangeSlider.
sgcNavigationTsgcWinFormsNavigationBar, ...NavigationTabs, ...NavigationBreadcrumb, ...DockManagerTsgcWpfNavigationBar, ...NavigationTabs, ...NavigationBreadcrumb, ...DockManagerTsgcAvaloniaNavigationBar, ...NavigationTabs, ...NavigationBreadcrumb, ...DockManager
sgcToolbarTsgcWinFormsRibbon, TsgcWinFormsToolbar, TsgcWinFormsStatusBarTsgcWpfRibbon, TsgcWpfToolbar, TsgcWpfStatusBarTsgcAvaloniaRibbon, TsgcAvaloniaToolbar, TsgcAvaloniaStatusBar
sgcWizardTsgcWinFormsWizard, TsgcWinFormsStepperTsgcWpfWizard, TsgcWpfStepperTsgcAvaloniaWizard, TsgcAvaloniaStepper
sgcRichEditTsgcWinFormsRichEditTsgcWpfRichEditTsgcAvaloniaRichEdit
sgcPDF viewerTsgcPDFViewerControlTsgcWpfPDFViewerTsgcAvaloniaPDFViewer
sgcReports designerTsgcWinFormsReportDesignerTsgcWpfReportDesignerTsgcAvaloniaReportDesigner
sgcReports previewTsgcReportPreviewVCL (a WinForms Panel)TsgcWpfReportPreviewTsgcAvaloniaReportPreview
sgcUICoreTsgcWinFormsAIQueryBar, TsgcWinFormsValidationSummaryTsgcWpfAIQueryBar, TsgcWpfValidationSummaryTsgcAvaloniaAIQueryBar, TsgcAvaloniaValidationSummary

The namespace is one per product (esegece.sgcGrid, esegece.sgcCharts, ...) and the same for the three hosts, except the WinForms map, which is in esegece.sgcMaps.WinForms. The host controls come in the packages esegece.<Product>.WinForms, .Wpf and .Avalonia; the WinForms map tile of the dashboard is in esegece.sgcDashboard.Maps.WinForms. On WPF and Avalonia every product except sgcReports is declared in XAML with one namespace URI, xmlns:sgc="https://www.esegece.com/sgcui"; for sgcReports use a clr-namespace mapping.

The Members That Change in a Port

Everything else on the controls keeps its name. These are the places where the host shows through.

Concern WinForms WPF Avalonia
Bind a list to the gridDataSource (and DataMember)ItemsSource (DependencyProperty)ItemsSource (StyledProperty)
Grid binding membersAutoGenerateColumns, KeyMember, SelectedItem, ItemsBinding, and ItemsWriteBack (Registered), the same on the three hosts
Current item syncnoneIsSynchronizedWithCurrentItemnone
Grid columns and dataColumns (TsgcGridColumns), Data (TsgcGridData)
Chart modelChart (TsgcChart)
Planner modelData (TsgcPlannerData), Options (TsgcPlannerSchedulerOptions)
Themecontrol Theme (TsgcUITheme; utDefault follows sgcUIApplication.Theme)the same, plus sgcUICore_Theme_Avalonia.sgcUIApplyThemeToWindow
UI threadControl.BeginInvokeDispatcher.BeginInvokeDispatcher.UIThread.Post
Application startApplication.Run(form)new Application().Run(window)an Application class with a theme, and StartWithClassicDesktopLifetime
Target framework of the appnet8.0-windows (also net472)net8.0-windows (also net472)net8.0, net9.0, net10.0

HOST Lines and SAME Lines

Every excerpt is copied from the MigrateToAvalonia sample, which compiles in Release for the Registered and Community editions with 0 warnings and passes its self test on WinForms, WPF and Avalonia. In the host files, a line marked HOST differs between the three hosts and a line marked SAME is identical in all of them.

Written once: the shared layer

The shared class library references only the model packages esegece.sgcGrid, esegece.sgcCharts, esegece.sgcPlanner and esegece.sgcUICore, never a host package. The grid columns, with grouping and summaries, are configured against TsgcGridColumns:

Shared\TMigrateModel.cs
// the grid columns, grouped by Region, with a sum of Amount in the
// footer and in every group row; TsgcGridColumns is the Columns of
// TsgcWinFormsGrid, TsgcWpfGrid and TsgcAvaloniaGrid
public static void ConfigureGrid(TsgcGridColumns aColumns)
{
    aColumns.BeginUpdate();
    try
    {
        aColumns.Clear();
        aColumns.Add(nameof(TOrder.Id), "Order", TsgcGridDataType.gdtInteger).Width = 80;
        aColumns.Add(nameof(TOrder.Customer), "Customer").Width = 180;
        aColumns.Add(nameof(TOrder.Region), "Region").GroupIndex = 0;
        TsgcGridColumn oDate = aColumns.Add(nameof(TOrder.Date), "Date", TsgcGridDataType.gdtDateTime);
        oDate.Width = 110;
        oDate.DisplayFormat = "yyyy-mm-dd";
        TsgcGridColumn oAmount = aColumns.Add(nameof(TOrder.Amount), "Amount", TsgcGridDataType.gdtFloat);
        oAmount.Width = 120;
        oAmount.DisplayFormat = "#,##0.00";
        oAmount.SummaryType = TsgcGridSummaryType.gsuSum;
        oAmount.SummaryFormat = "\"Total: \"#,##0.00";
        aColumns.Add(nameof(TOrder.Status), "Status").Width = 100;
    }
    finally
    {
        aColumns.EndUpdate();
    }
}

The chart is a TsgcChart model, its pie series fed live from an ObservableCollection through TsgcChartDataSource_Items:

Shared\TMigrateModel.cs
// the chart model: one pie series fed by RegionTotals through the
// items data source (live: a changed Amount updates its slice)
Chart = new TsgcChart();
Chart.Title.Text = "Amount by region";
Chart.Title.Visible = true;
Chart.Legend.Position = TsgcChartLegendPosition.clpTop;
Pie = new TsgcChartSeries_Pie();
Pie.Title = "Amount";
Pie.Chart = Chart;
FChartSource = new TsgcChartDataSource_Items();
FChartSource.Series = Pie;
FChartSource.LabelMember = nameof(TRegionTotal.Region);
FChartSource.YMember = nameof(TRegionTotal.Amount);
FChartSource.Items = RegionTotals;

One theme for every sgcUI control of the application:

Shared\TMigrateModel.cs
// the theme of every sgcUI control of the application (their Theme is
// utDefault, so they follow sgcUIApplication.Theme), and one accent
public static void ApplyTheme()
{
    sgcUIApplication.Theme = TsgcUITheme.utLight;
    sgcUIApplication.AccentColor = 0xFF2563EB;
}

The one host service: posting to the UI thread

Bound collections change on the UI thread. The shared layer does not know the host, so it receives the post call as a delegate:

Shared\TMigrateModel.cs
// aPost runs an action on the UI thread; it is the only host specific
// service of the layer (WinForms Control.BeginInvoke, WPF
// Dispatcher.BeginInvoke, Avalonia Dispatcher.UIThread.Post)
public TMigrateModel(Action<Action> aPost)
Shared\TMigrateModel.cs
// a feed on a worker thread: the order is built there and posted to the
// UI thread with aPost, because bound collections change on the UI thread
public Task AddOrderFromWorker()
{
    return Task.Run(() =>
    {
        TOrder oOrder = DoNewOrder();
        FPost(() => DoAdd(oOrder));
    });
}

The control fields differ only in type

Before: the WinForms or the WPF types. After: the Avalonia types.

WinForms\TMigrateForm.cs
// HOST: the WinForms control types
private readonly TsgcWinFormsGrid FGrid;
private readonly TsgcWinFormsChart FChart;
private readonly TsgcWinFormsScheduler FScheduler;
Wpf\TMigrateWindow.cs
// HOST: the WPF control types
private readonly TsgcWpfGrid FGrid;
private readonly TsgcWpfChart FChart;
private readonly TsgcWpfScheduler FScheduler;
Avalonia\TMigrateWindow.cs
// HOST: the Avalonia control types
private readonly TsgcAvaloniaGrid FGrid;
private readonly TsgcAvaloniaChart FChart;
private readonly TsgcAvaloniaScheduler FScheduler;

Binding the screen: the same block on every host

Two lines are marked HOST: the UI thread call and the list binding. The rest is the same text in the three files.

WinForms\TMigrateForm.cs
// HOST: WinForms reaches the UI thread with Control.BeginInvoke
FModel = new TMigrateModel(aAction => BeginInvoke(aAction));

// SAME on every host: the shared columns, footer and group sums
TMigrateModel.ConfigureGrid(FGrid.Columns);
FGrid.ShowFooter = true;
FGrid.GroupSummaries = true;
// HOST: WinForms binds with DataSource (WPF and Avalonia: ItemsSource)
FGrid.DataSource = FModel.Orders;
// SAME on every host: the shared chart model and planner data
FChart.Chart = FModel.Chart;
TMigrateModel.ConfigureScheduler(FScheduler.Options);
FScheduler.Data = FModel.Planner;
FScheduler.View = TsgcPlannerSchedulerViewKind.psvWorkWeek;
FScheduler.Date = TMigrateModel.CS_WEEK;
Wpf\TMigrateWindow.cs
// HOST: WPF reaches the UI thread with Dispatcher.BeginInvoke
FModel = new TMigrateModel(aAction => Dispatcher.BeginInvoke(aAction));

// SAME on every host: the shared columns, footer and group sums
TMigrateModel.ConfigureGrid(FGrid.Columns);
FGrid.ShowFooter = true;
FGrid.GroupSummaries = true;
// HOST: WPF binds with ItemsSource (WinForms: DataSource)
FGrid.ItemsSource = FModel.Orders;
// SAME on every host: the shared chart model and planner data
FChart.Chart = FModel.Chart;
TMigrateModel.ConfigureScheduler(FScheduler.Options);
FScheduler.Data = FModel.Planner;
FScheduler.View = TsgcPlannerSchedulerViewKind.psvWorkWeek;
FScheduler.Date = TMigrateModel.CS_WEEK;
Avalonia\TMigrateWindow.cs
// HOST: Avalonia reaches the UI thread with Dispatcher.UIThread.Post
FModel = new TMigrateModel(aAction => Dispatcher.UIThread.Post(aAction));

// SAME on every host: the shared columns, footer and group sums
TMigrateModel.ConfigureGrid(FGrid.Columns);
FGrid.ShowFooter = true;
FGrid.GroupSummaries = true;
// HOST: Avalonia binds with ItemsSource, as WPF (WinForms: DataSource)
FGrid.ItemsSource = FModel.Orders;
// SAME on every host: the shared chart model and planner data
FChart.Chart = FModel.Chart;
TMigrateModel.ConfigureScheduler(FScheduler.Options);
FScheduler.Data = FModel.Planner;
FScheduler.View = TsgcPlannerSchedulerViewKind.psvWorkWeek;
FScheduler.Date = TMigrateModel.CS_WEEK;

The application start

The start code is host code by nature. The theme call is the same on the three hosts.

WinForms\Program.cs
// HOST: the WinForms application start
Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// SAME on every host: one theme for every sgcUI control
TMigrateModel.ApplyTheme();
TMigrateForm oForm = new TMigrateForm(vTest ? vFolder : null);
Application.Run(oForm);
return oForm.ExitCode;
Wpf\Program.cs
// HOST: the WPF application start
Application oApp = new Application { ShutdownMode = ShutdownMode.OnMainWindowClose };
// SAME on every host: one theme for every sgcUI control
TMigrateModel.ApplyTheme();
TMigrateWindow oWindow = new TMigrateWindow(vTest ? vFolder : null);
oApp.Run(oWindow);
return oWindow.ExitCode;
Avalonia\Program.cs
// HOST: Avalonia needs an Application class with a theme
internal sealed class TMigrateApp : Application
{
    public static string TestFolder;

    public override void Initialize()
    {
        Styles.Add(new FluentTheme());
        RequestedThemeVariant = ThemeVariant.Light;
    }

    public override void OnFrameworkInitializationCompleted()
    {
        // SAME on every host: one theme for every sgcUI control
        TMigrateModel.ApplyTheme();
        IClassicDesktopStyleApplicationLifetime oDesktop =
            ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
        if (oDesktop != null)
            oDesktop.MainWindow = new TMigrateWindow(TestFolder);
        base.OnFrameworkInitializationCompleted();
    }
}

Eight Steps per Screen

The Windows app keeps working after every step, so the move can be done one screen at a time.

1

Extract the shared layer

Move the data and the control configuration into a class library that references only the sgcUI model packages (esegece.sgcGrid, esegece.sgcCharts, esegece.sgcPlanner, esegece.sgcUICore). Write it against TsgcGridColumns, TsgcChart, TsgcPlannerData and TsgcPlannerSchedulerOptions.

2

Call it from Windows

Make the existing WinForms or WPF screen call that library. It keeps working on Windows as before.

3

Create the Avalonia app

A net8.0 project with Avalonia.Desktop, Avalonia.Themes.Fluent and the esegece.<Product>.Avalonia packages you need.

4

Swap the control types

TsgcWinForms* and TsgcWpf* become TsgcAvalonia*, with the exceptions listed in the mapping table.

5

Port the window

A Form with a TableLayoutPanel, or a WPF Window with a Grid, becomes an Avalonia Window with a Grid. WPF XAML moves to .axaml and keeps the sgc namespace URI.

6

WinForms only: the binding

Change DataSource to ItemsSource. WPF code already uses ItemsSource.

7

The UI thread call

BeginInvoke or Dispatcher.BeginInvoke becomes Dispatcher.UIThread.Post.

8

Check every host

Run the same checks on each host, as the sample's /selftest does, and compare the saved grid layout JSON: it must be the same text.

What Does Not Map One to One

Read this before you plan the move.

MigrateToAvalonia

One screen with a grid of 40 orders grouped by region, a pie chart of the amount by region and a work week scheduler, shown by three hosts from one shared layer.

Where it is

In the sgcUI .NET demos, folder Suite\MigrateToAvalonia of the sgcUI_Demos.sln solution: Shared, WinForms, Wpf, Avalonia and a readme.html. In the demo download the projects reference the sgcUI NuGet packages.

How to run it

dotnet build Avalonia\sgcUIMigrate.Avalonia.csproj -c Release, and the same for WinForms\sgcUIMigrate.WinForms.csproj and Wpf\sgcUIMigrate.Wpf.csproj. Add -p:SgcCommunity=true for the Community edition. The Avalonia host also runs on macOS and Linux.

Self test

sgcUIMigrate.<Host>.exe /selftest [folder] checks the rows, the grouping, the pie, the scheduler, an order added on a worker thread and a removed order, writes migrate_selftest_<host>.txt and migrate_layout_<host>.json, and exits with the number of failed checks. All 13 checks pass on the three hosts, and the three layout files are byte for byte the same.

To be fair to both routes: XPF aims to run an existing WPF application on macOS and Linux with its code and controls largely as they are, and sgcUI does not do that. With sgcUI you port each screen's window code to Avalonia, and the sgcUI parts of the screen carry over unchanged. Screens built on controls from other vendors need those controls replaced first, on Windows, before the move. sgcUI is the path when your screens use, or can move to, the sgcUI grid, charts, planner, pivot, diagram and the other products above, and you want native Avalonia controls with full source code and no runtime layer in between. For the products and editions see the sgcUI product page.

Best value: All-AccessEvery eSeGeCe product, Premium Support included, from €1,059/year.
See All-Access pricing

Move Your First Screen Today

Add the free Community packages from NuGet, or the Registered packages for every feature and full source code, and port your first screen with the MigrateToAvalonia sample next to it.