Hvorfor du skal migrere nu — ikke i morgen
Okay, lad os starte med den ubehagelige sandhed: Microsofts officielle support for Xamarin sluttede den 1. maj 2024. Det er ikke en fremtidig trussel — det er allerede sket. Og konsekvenserne? De er langt alvorligere end bare manglende opdateringer fra Microsoft.
Google kræver nu, at apps skal målrette mindst Android 14 (API 34+) for at publicere opdateringer på Play Store. Xamarin understøtter kun op til Android 13. Apple kræver iOS 17 som minimum — Xamarin understøtter kun op til iOS 16. Det betyder i praksis, at du ikke kan udgive opdateringer til dine eksisterende Xamarin-apps i app-butikkerne. Dine apps kan blive fjernet, og inaktivitet kan negativt påvirke hele din udviklerkonto.
Og så er der sikkerhedssiden. Xamarin-frameworket modtager ikke længere sikkerhedsopdateringer. Nye sårbarheder, der opdages i frameworket eller dets afhængigheder, vil simpelthen forblive uadresserede — og det eksponerer dine apps og brugere for potentielle risici.
Men her er den gode nyhed: .NET MAUI er modnet enormt. Med .NET 10, der blev udgivet som en Long-Term Support (LTS)-udgivelse i november 2025, er platformen stabil, velunderstøttet og klar til produktion. Migreringen er ikke længere et spring ud i det ukendte — det er en veldokumenteret rejse med afprøvede værktøjer og mønstre.
Denne guide tager dig igennem hele processen. Fra den indledende audit af din eksisterende app til den endelige validering af din migrerede .NET MAUI-applikation — med konkrete kodeeksempler, praktiske tips og løsninger på de mest almindelige faldgruber.
Forstå de arkitektoniske forskelle
Før du begynder at flytte kode, er det afgørende at forstå, hvordan .NET MAUI adskiller sig fra Xamarin.Forms på et arkitektonisk niveau. Det her er ikke bare en navneændring — der er fundamentale forskelle i projektstruktur, renderingsarkitektur og API-design.
Fra multi-projekt til single-projekt
I Xamarin.Forms havde du typisk separate projekter for hver platform: et iOS-projekt, et Android-projekt, et eventuelt UWP-projekt og et delt .NET Standard-projekt. Det betød separate .csproj-filer, separate platformkonfigurationer og (lad os være ærlige) ofte forvirrende build-opsætninger.
.NET MAUI introducerer single-projekt arkitekturen. Alle platforme samles i ét projekt med en enkelt .csproj-fil, der bruger multi-targeting. Platformspecifik kode organiseres i mapper under Platforms/-mappen:
MinApp/
├── App.xaml
├── App.xaml.cs
├── MauiProgram.cs # Ny: Appens indgangspunkt
├── MainPage.xaml
├── MainPage.xaml.cs
├── Platforms/
│ ├── Android/
│ │ ├── AndroidManifest.xml
│ │ └── MainActivity.cs
│ ├── iOS/
│ │ ├── AppDelegate.cs
│ │ └── Info.plist
│ ├── MacCatalyst/
│ │ └── AppDelegate.cs
│ └── Windows/
│ └── App.xaml
├── Resources/
│ ├── Fonts/
│ ├── Images/
│ └── Styles/
└── MinApp.csproj # Én projektfil for alle platforme
Denne struktur er dramatisk simplere at vedligeholde. Fælles ressourcer som fonte, billeder og farver deles automatisk på tværs af platforme, og du slipper for at synkronisere NuGet-pakker mellem multiple projekter. Det alene er en kæmpe gevinst i hverdagen.
Fra Renderers til Handlers
Den mest fundamentale arkitektoniske ændring er overgangen fra renderers til handlers. I Xamarin.Forms fungerede en renderer som en bro mellem den cross-platform kontrol og den native kontrol. Problemet var, at renderers var tæt koblet til frameworket og skabte ekstra overhead — på Android oprettede ViewRenderer f.eks. en ekstra ViewGroup som parent-element til positionering.
MAUI-handlers inverterer denne relation. De er lettere, hurtigere og nemmere at tilpasse. En handler opretter ikke et ekstra parent-element, hvilket reducerer det visuelle hierarki og forbedrer ydeevnen. Derudover bruger handlers et mapper-baseret system til property-ændringer, i stedet for den monolitiske OnElementPropertyChanged-metode fra renderers.
Kort sagt: handlers gør det hele enklere.
Fra DependencyService til Dependency Injection
Xamarin.Forms brugte DependencyService til platformspecifikke implementeringer — en simpel service locator, der registrerede og resolvede interfaces via attributter. .NET MAUI erstatter dette med ægte dependency injection via Microsoft.Extensions.DependencyInjection, den samme DI-container der bruges i ASP.NET Core.
// ❌ Xamarin.Forms: DependencyService
[assembly: Dependency(typeof(DeviceInfoService))]
public class DeviceInfoService : IDeviceInfoService
{
public string GetDeviceModel() => Build.Model;
}
// Brug:
var service = DependencyService.Get<IDeviceInfoService>();
// ✅ .NET MAUI: Dependency Injection
public class DeviceInfoService : IDeviceInfoService
{
public string GetDeviceModel() => DeviceInfo.Current.Model;
}
// Registrering i MauiProgram.cs:
builder.Services.AddSingleton<IDeviceInfoService, DeviceInfoService>();
// Brug via constructor injection:
public class MinViewModel
{
private readonly IDeviceInfoService _deviceInfo;
public MinViewModel(IDeviceInfoService deviceInfo)
{
_deviceInfo = deviceInfo;
}
}
Denne ændring giver bedre testbarhed, klarere afhængigheder og (ikke mindst) en langt bedre integration med det bredere .NET-økosystem.
Trin 1: Auditér din eksisterende Xamarin-app
Migreringsprocessen starter ikke med kode — den starter med analyse. Helt ærligt, en grundig audit af din eksisterende applikation er nok den vigtigste forudsætning for en succesfuld migrering. Spring ikke dette trin over.
Kortlæg dine afhængigheder
Gennemgå alle NuGet-pakker i din løsning. For hver pakke skal du afgøre:
- Er pakken kompatibel med .NET MAUI? Mange populære pakker har udgivet MAUI-versioner.
- Findes der et alternativ? Hvis pakken ikke er migreret, find en erstatning.
- Kan du undvære den? Nogle pakker løser problemer, der nu er håndteret af MAUI eller .NET selv.
Her er en oversigt over almindelige Xamarin-pakker og deres MAUI-ækvivalenter:
// Xamarin.Forms → .NET MAUI ækvivalent
// ─────────────────────────────────────────────────────────
// Xamarin.Forms → Microsoft.Maui.Controls
// Xamarin.Essentials → Microsoft.Maui.Essentials (indbygget)
// Xamarin.CommunityToolkit → CommunityToolkit.Maui
// Xamarin.Forms.Maps → Microsoft.Maui.Controls.Maps
// Plugin.Connectivity → Microsoft.Maui.Networking (indbygget)
// Plugin.Permissions → Microsoft.Maui.ApplicationModel (indbygget)
// SkiaSharp.Views.Forms → SkiaSharp.Views.Maui.Controls
// FFImageLoading → Ingen direkte erstatning*
// Rg.Plugins.Popup → CommunityToolkit.Maui (Popup)
// Prism.Forms → Prism.Maui
// * FFImageLoading er ikke migreret. Brug i stedet:
// - MAUI's indbyggede billedhåndtering (markant forbedret)
// - Glide/SDWebImage via handlers
// - CommunityToolkit.Maui.MediaElement til video
Identificér custom renderers
Custom renderers er den mest arbejdskrævende del af migreringen. Tæl dem og vurdér deres kompleksitet:
- 1-5 simple renderers: Få dages arbejde. Migrér direkte til handlers.
- 5-20 renderers: En til to ugers arbejde. Overvej at genbruge renderers midlertidigt.
- 20+ renderers: Betydeligt arbejde. Her er en faseopdelt tilgang stort set nødvendig.
Dokumentér platformspecifik kode
Gennemgå al kode der bruger Device.RuntimePlatform, conditional compilation (#if), eller platform-specifikke API'er. Denne kode skal tilpasses til MAUI's nye platformabstraktioner.
Trin 2: Brug .NET Upgrade Assistant
Microsoft har udviklet .NET Upgrade Assistant specifikt til at hjælpe med Xamarin-til-MAUI migrering. Værktøjet automatiserer mange af de mekaniske ændringer og kan spare dig for adskillige timer med repetitivt arbejde.
Installation
Du kan installere .NET Upgrade Assistant som et kommandolinjeværktøj:
# Installér .NET Upgrade Assistant
dotnet tool install -g upgrade-assistant
# Verificér installation
upgrade-assistant --version
# Kør mod din Xamarin.Forms-løsning
upgrade-assistant upgrade MinXamarinApp.sln
Alternativt kan du installere det som en Visual Studio-udvidelse. I Visual Studio 2022 går du til Extensions → Manage Extensions, søger efter ".NET Upgrade Assistant" og installerer.
Hvad værktøjet gør automatisk
Upgrade Assistant håndterer en hel del transformationer for dig:
- Konverterer projektfiler til SDK-style
.csproj-format - Opdaterer target frameworks til
net10.0-androidognet10.0-ios - Fjerner Xamarin.Forms og Xamarin.Essentials NuGet-pakker
- Erstatter Xamarin.CommunityToolkit med .NET MAUI Community Toolkit
- Konverterer SkiaSharp-pakker til MAUI-kompatible versioner
- Tilbyder quick actions til namespace-konvertering
Hvad værktøjet IKKE gør
Det er vigtigt at kende begrænsningerne — ellers får du en overraskelse:
- Konverterer ikke custom renderers til handlers
- Understøtter ikke Xamarin.Native (kun Xamarin.Forms)
- Håndterer ikke komplekse tredjepartsbiblioteker automatisk
- Migrerer ikke DependencyService til DI automatisk
- Effektiviteten falder med projektets kompleksitet
Brug Upgrade Assistant som et udgangspunkt — ikke som en færdig løsning. Det håndterer typisk 50-70% af det mekaniske arbejde, men den resterende del kræver manuel indsats og omtanke.
Trin 3: Namespace-ændringer
En af de mest omfattende (men heldigvis mekaniske) ændringer er namespace-konverteringen. Alle Xamarin.Forms-namespaces skal erstattes med deres MAUI-ækvivalenter. Her er de vigtigste:
// Namespace-konverteringer
// ─────────────────────────────────────────────────────────────────
// Xamarin.Forms → Microsoft.Maui.Controls
// Xamarin.Forms.Xaml → Microsoft.Maui.Controls.Xaml
// Xamarin.Forms.Maps → Microsoft.Maui.Controls.Maps
// Xamarin.Forms.Shapes → Microsoft.Maui.Controls.Shapes
// Xamarin.Essentials → Microsoft.Maui.Devices
// Microsoft.Maui.Networking
// Microsoft.Maui.ApplicationModel
// Microsoft.Maui.Media
// Microsoft.Maui.Storage
// XAML namespace-ændringer:
// xmlns="http://xamarin.com/schemas/2014/forms"
// → xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
// xmlns:xct="http://xamarin.com/schemas/toolkit"
// → xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
I XAML-filer skal du også opdatere rod-namespace'et. Her er et typisk eksempel på en side før og efter migrering:
<!-- ❌ Xamarin.Forms XAML -->
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MinApp.Views.ProfilSide">
<StackLayout Padding="20">
<Label Text="Min Profil" FontSize="Title" />
<Entry Text="{Binding Navn}" />
<Button Text="Gem" Command="{Binding GemCommand}" />
</StackLayout>
</ContentPage>
<!-- ✅ .NET MAUI XAML -->
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MinApp.Views.ProfilSide">
<VerticalStackLayout Padding="20">
<Label Text="Min Profil" FontSize="Title" />
<Entry Text="{Binding Navn}" />
<Button Text="Gem" Command="{Binding GemCommand}" />
</VerticalStackLayout>
</ContentPage>
Bemærk, at StackLayout stadig virker i MAUI, men det anbefales at skifte til VerticalStackLayout eller HorizontalStackLayout, da de er markant hurtigere. De nye layout-kontroller udfører kun ét gennemløb af deres børn (single-pass layout), i modsætning til StackLayout, der bruger to gennemløb. Den forskel kan du faktisk mærke i praksis.
Trin 4: Migrér Custom Renderers til Handlers
Det her er typisk den mest tidskrævende del af migreringen. Der er to strategier, og du kan sagtens bruge dem begge i samme projekt.
Strategi A: Genbrug eksisterende renderers (hurtig løsning)
.NET MAUI understøtter genbrugelige renderers via compatibility-shims. Det gør det muligt at bruge dine eksisterende renderers med minimale ændringer — en perfekt strategi for at komme hurtigt i gang:
// Trin 1: Opdatér namespaces i din renderer
// Fjern: using Xamarin.Forms;
// Fjern: using Xamarin.Forms.Platform.Android;
// Tilføj: using Microsoft.Maui.Controls;
// Tilføj: using Microsoft.Maui.Controls.Compatibility;
// Tilføj: using Microsoft.Maui.Controls.Compatibility.Platform.Android;
// Trin 2: Fjern [ExportRenderer]-attributten
// Den er ikke nødvendig i MAUI
// Trin 3: Registrér rendereren i MauiProgram.cs
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>()
.ConfigureMauiHandlers(handlers =>
{
#if ANDROID
handlers.AddHandler(
typeof(AfrundedEntry),
typeof(AfrundedEntryRenderer));
#elif IOS
handlers.AddHandler(
typeof(AfrundedEntry),
typeof(AfrundedEntryRenderer));
#endif
});
return builder.Build();
}
Denne tilgang virker for de fleste renderers, men husk at compatibility-shims ikke nødvendigvis vil blive understøttet i fremtidige .NET-versioner. Det er en overgangsløsning — ikke en permanent strategi.
Strategi B: Migrér til native handlers (anbefalet)
For nye projekter eller som en del af en faseopdelt migrering bør du konvertere renderers til handlers. Her er et komplet eksempel med en afrundet Entry-kontrol:
// 1. Definér interfacet
public interface IAfrundedEntry : IView
{
string Text { get; }
Color TextColor { get; }
Color BaggrundsKantFarve { get; }
float HjørneRadius { get; }
}
// 2. Opret den cross-platform kontrol
public class AfrundedEntry : View, IAfrundedEntry
{
public static readonly BindableProperty TextProperty =
BindableProperty.Create(nameof(Text), typeof(string),
typeof(AfrundedEntry), string.Empty);
public static readonly BindableProperty BaggrundsKantFarveProperty =
BindableProperty.Create(nameof(BaggrundsKantFarve), typeof(Color),
typeof(AfrundedEntry), Colors.Gray);
public static readonly BindableProperty HjørneRadiusProperty =
BindableProperty.Create(nameof(HjørneRadius), typeof(float),
typeof(AfrundedEntry), 10f);
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public Color BaggrundsKantFarve
{
get => (Color)GetValue(BaggrundsKantFarveProperty);
set => SetValue(BaggrundsKantFarveProperty, value);
}
public float HjørneRadius
{
get => (float)GetValue(HjørneRadiusProperty);
set => SetValue(HjørneRadiusProperty, value);
}
}
// 3. Opret Android-handleren
public partial class AfrundedEntryHandler :
ViewHandler<IAfrundedEntry, AppCompatEditText>
{
public static readonly IPropertyMapper<IAfrundedEntry,
AfrundedEntryHandler> Mapper = new PropertyMapper
<IAfrundedEntry, AfrundedEntryHandler>(ViewMapper)
{
[nameof(IAfrundedEntry.Text)] = MapText,
[nameof(IAfrundedEntry.BaggrundsKantFarve)] = MapBaggrundsKantFarve,
[nameof(IAfrundedEntry.HjørneRadius)] = MapHjørneRadius
};
public AfrundedEntryHandler() : base(Mapper) { }
protected override AppCompatEditText CreatePlatformView()
{
return new AppCompatEditText(Context);
}
private static void MapText(AfrundedEntryHandler handler,
IAfrundedEntry entry)
{
handler.PlatformView.Text = entry.Text;
}
private static void MapBaggrundsKantFarve(AfrundedEntryHandler handler,
IAfrundedEntry entry)
{
var drawable = new GradientDrawable();
drawable.SetStroke(2, entry.BaggrundsKantFarve
.ToPlatform());
handler.PlatformView.Background = drawable;
}
private static void MapHjørneRadius(AfrundedEntryHandler handler,
IAfrundedEntry entry)
{
if (handler.PlatformView.Background is GradientDrawable drawable)
{
drawable.SetCornerRadius(entry.HjørneRadius
* handler.PlatformView.Context.Resources
.DisplayMetrics.Density);
}
}
}
Tilpas eksisterende handlers med AppendToMapping
I mange tilfælde behøver du slet ikke oprette en helt ny handler. Det er vigtigt at vide. .NET MAUI giver dig mulighed for at tilpasse eksisterende handlers direkte via AppendToMapping, og det er ideelt til simple visuelle tilpasninger:
// I MauiProgram.cs eller en dedikeret konfigurationsklasse
Microsoft.Maui.Handlers.EntryHandler.Mapper
.AppendToMapping("AfrundedKanter", (handler, view) =>
{
#if ANDROID
handler.PlatformView.SetBackgroundColor(
Android.Graphics.Color.Transparent);
var drawable = new GradientDrawable();
drawable.SetCornerRadius(24f);
drawable.SetStroke(2, Android.Graphics.Color.Gray);
drawable.SetColor(Android.Graphics.Color.White);
handler.PlatformView.Background = drawable;
handler.PlatformView.SetPadding(30, 20, 30, 20);
#elif IOS
handler.PlatformView.BorderStyle =
UIKit.UITextBorderStyle.None;
handler.PlatformView.Layer.CornerRadius = 12f;
handler.PlatformView.Layer.BorderWidth = 1f;
handler.PlatformView.Layer.BorderColor =
UIKit.UIColor.Gray.CGColor;
handler.PlatformView.ClipsToBounds = true;
#endif
});
Denne tilgang er markant enklere end at oprette en komplet handler og dækker de fleste use cases for visuel tilpasning. Jeg bruger den selv som førstevalg, og det er sjældent, at jeg har brug for en fuld custom handler.
Trin 5: Migrér app-livscyklus og navigation
App-indgangspunktet har ændret sig fundamentalt. I Xamarin.Forms havde du App.xaml.cs med OnStart(), OnSleep() og OnResume(). .NET MAUI introducerer MauiProgram.cs som det primære indgangspunkt, og livscyklushåndtering er blevet mere fleksibel:
// MauiProgram.cs — dit nye indgangspunkt
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
// Registrer dine services
builder.Services.AddSingleton<INavigationService,
ShellNavigationService>();
builder.Services.AddSingleton<IDataService, DataService>();
// Registrer sider og viewmodels
builder.Services.AddTransient<ProfilSide>();
builder.Services.AddTransient<ProfilViewModel>();
builder.Services.AddTransient<IndstillingerSide>();
builder.Services.AddTransient<IndstillingerViewModel>();
return builder.Build();
}
}
// App.xaml.cs — livscyklushåndtering
public partial class App : Application
{
public App()
{
InitializeComponent();
}
// I .NET 10 MAUI bruges CreateWindow i stedet for MainPage
protected override Window CreateWindow(
IActivationState? activationState)
{
var window = new Window(new AppShell());
// Livscyklushændelser på vinduesniveau
window.Created += (s, e) =>
Debug.WriteLine("Vindue oprettet");
window.Activated += (s, e) =>
Debug.WriteLine("App aktiveret");
window.Deactivated += (s, e) =>
Debug.WriteLine("App deaktiveret");
window.Stopped += (s, e) =>
Debug.WriteLine("App stoppet");
window.Resumed += (s, e) =>
Debug.WriteLine("App genoptaget");
window.Destroying += (s, e) =>
Debug.WriteLine("Vindue destrueres");
return window;
}
}
Shell-navigation
Navigationsmodellen i .NET MAUI Shell ligner Xamarin.Forms Shell, men der er vigtige forskelle. URI-baseret navigation fungerer stort set ens, men registrering af routes er forenklet:
// Route-registrering
Routing.RegisterRoute(nameof(ProfilSide), typeof(ProfilSide));
Routing.RegisterRoute(nameof(DetaljerSide), typeof(DetaljerSide));
// Navigation med parametre
await Shell.Current.GoToAsync($"{nameof(DetaljerSide)}",
new Dictionary<string, object>
{
["ProduktId"] = valgtProdukt.Id,
["ProduktNavn"] = valgtProdukt.Navn
});
// Modtag parametre via QueryProperty (Xamarin)
// eller IQueryAttributable (MAUI)
public partial class DetaljerSide : ContentPage, IQueryAttributable
{
public void ApplyQueryAttributes(
IDictionary<string, object> query)
{
if (query.TryGetValue("ProduktId", out var id))
{
// Indlæs produktdetaljer
}
}
}
Trin 6: Ressourcer og stilarter
Ressourcehåndteringen er forbedret markant i .NET MAUI. I stedet for platformspecifikke ressourcemapper bruger MAUI en centraliseret Resources/-mappe med automatisk platformtilpasning. Det er en af de ting, der bare fungerer bedre:
<!-- .csproj: Ressource-inkludering sker automatisk -->
<ItemGroup>
<!-- Billeder konverteres automatisk til
platformspecifikke formater -->
<MauiImage Include="Resources\Images\*" />
<!-- Fonte registreres i MauiProgram.cs -->
<MauiFont Include="Resources\Fonts\*" />
<!-- App-ikon genereres til alle platforme
fra én SVG-fil -->
<MauiIcon Include="Resources\AppIcon\appicon.svg"
ForegroundFile="Resources\AppIcon\appiconfg.svg"
Color="#512BD4" />
<!-- Splash screen fra én SVG -->
<MauiSplashScreen Include="Resources\Splash\splash.svg"
Color="#512BD4"
BaseSize="128,128" />
</ItemGroup>
Stilarter og temaer fungerer ligesom i Xamarin.Forms med ResourceDictionary, men MAUI understøtter nu bedre dynamisk tema-skift og system-mørk tilstand out of the box.
Trin 7: Test og validering
Når koden er migreret, begynder den kritiske testfase. Og ja, det her trin er mindst lige så vigtigt som selve migreringen.
Ydeevne-benchmarking
Etablér ydelsesbaselines før migreringen, så du kan sammenligne direkte. Mål som minimum:
- Opstartstid (cold start og warm start)
- Hukommelsesforbrug ved typisk brug
- Scrolling-ydeevne i lister med mange elementer
- Navigationstid mellem sider
// Simpel ydeevnemåling i din app
public static class PerformanceLogger
{
private static readonly Stopwatch _stopwatch = new();
private static readonly Dictionary<string, TimeSpan> _markers = new();
public static void StartMeasure(string label)
{
_stopwatch.Restart();
Debug.WriteLine($"[PERF] Start: {label}");
}
public static void EndMeasure(string label)
{
_stopwatch.Stop();
_markers[label] = _stopwatch.Elapsed;
Debug.WriteLine(
$"[PERF] Slut: {label} — {_stopwatch.ElapsedMilliseconds}ms");
}
public static void LogAllMarkers()
{
Debug.WriteLine("=== Ydeevnerapport ===");
foreach (var (label, tid) in _markers)
{
Debug.WriteLine($" {label}: {tid.TotalMilliseconds:F1}ms");
}
}
}
Enhedstest med .NET MAUI
De fleste af dine eksisterende enhedstests bør fungere med minimale ændringer, da de typisk tester forretningslogik og viewmodels. Det er en af de ting, der gør migreringen lidt mere overkommelig. For UI-tests anbefales Appium med .NET-bindings eller det nye Microsoft.Maui.TestUtils-bibliotek:
// ViewModel-test (fungerer stort set uændret)
[TestClass]
public class ProfilViewModelTests
{
[TestMethod]
public async Task GemProfil_MedGyldigData_GemmerKorrekt()
{
// Arrange
var mockService = new Mock<IProfilService>();
mockService.Setup(s => s.GemProfilAsync(
It.IsAny<ProfilModel>()))
.ReturnsAsync(true);
var vm = new ProfilViewModel(mockService.Object);
vm.Navn = "Test Bruger";
vm.Email = "[email protected]";
// Act
await vm.GemProfilCommand.ExecuteAsync(null);
// Assert
mockService.Verify(s => s.GemProfilAsync(
It.Is<ProfilModel>(p =>
p.Navn == "Test Bruger" &&
p.Email == "[email protected]")),
Times.Once);
}
}
Test på fysiske enheder
Emulatorer er gode til hurtig udvikling, men de fanger ikke alt. Sørg for at teste på rigtige enheder på tværs af platforme og formfaktorer. Vær særligt opmærksom på:
- Ældre enheder med begrænset RAM
- Forskellige skærmstørrelser og opløsninger
- Enheder med nyeste OS-versioner (Android 16, iOS 18)
- Offline-scenarier og netværksskift
De mest almindelige faldgruber (og hvordan du undgår dem)
Efter at have set mange migreringsforløb er der en række problemer, der går igen. Her er de vigtigste — og deres løsninger.
1. CollectionView inde i ScrollView
Dette er et klassisk fejlmønster, der også eksisterer i Xamarin.Forms, men som er endnu mere problematisk i MAUI. Når en CollectionView placeres inde i en ScrollView, deaktiveres virtualiseringen, og alle elementer renderes på én gang. Det dræber ydeevnen. Brug i stedet et Grid med stjernerækker.
2. Manglende x:DataType for compiled bindings
Fra .NET 9 og fremefter er compiled bindings påkrævet for apps, der bruger fuld trimming eller Native AOT. Glem ikke at tilføje x:DataType til alle dine XAML-sider og DataTemplates. Uden det vil du opleve runtime-fejl eller tavse binding-fejl — og de kan være rigtigt svære at debugge.
3. ListView er deprecated i .NET 10
Hvis din Xamarin-app bruger ListView eller TableView, bør du migrere til CollectionView under migreringsprocessen. I .NET 10 er begge markeret som deprecated, og de nye CollectionView-handlers (standard fra .NET 10) er markant hurtigere og mere stabile.
4. Reflection-baseret kode og trimming
Fuld trimming fjerner kode, der ikke refereres statisk. Hvis du bruger reflection (f.eks. til serialisering, DI eller navigation), kan det give uventede runtime-fejl. Brug source generators og [DynamicallyAccessedMembers]-attributter til at bevare nødvendige typer:
// Markér typer der tilgås via reflection
[DynamicDependency(DynamicallyAccessedMemberTypes.All,
typeof(ProfilModel))]
public class DataService
{
public async Task<T?> HentDataAsync<T>(string endpoint)
{
var response = await _httpClient.GetAsync(endpoint);
// System.Text.Json bruger source generators
// i stedet for reflection
return await response.Content
.ReadFromJsonAsync<T>(AppJsonContext.Default
.GetTypeInfo(typeof(T)) as JsonTypeInfo<T>);
}
}
// Source generator til JSON-serialisering
[JsonSerializable(typeof(ProfilModel))]
[JsonSerializable(typeof(List<ProduktModel>))]
internal partial class AppJsonContext : JsonSerializerContext
{
}
5. StackLayout-ydeevneproblemer
Erstat StackLayout med VerticalStackLayout eller HorizontalStackLayout. De nye layouts bruger et enkelt layoutpass i stedet for to, hvilket giver mærkbar hastighedsforbedring — især i sider med mange indlejrede layouts. Det er en simpel ændring, der kan gøre en overraskende stor forskel.
6. Effektorer er deprecated
Hvis din Xamarin-app bruger Effect og PlatformEffect, bør du migrere til handler-tilpasninger via AppendToMapping eller ModifyMapping. Effektorer fungerer stadig via compatibility-laget, men understøttes muligvis ikke i fremtidige versioner.
Nyheder i .NET 10 der hjælper din migrering
.NET 10, udgivet i november 2025 som LTS, har flere funktioner der specifikt letter migreringen fra Xamarin.Forms:
- XAML Source Generator: Genererer stærkt typet kode fra dine XAML-filer på byggetidspunktet. Det forbedrer build-ydeevnen og giver bedre IntelliSense-support. Aktivér det med
<MauiXamlInflator>SourceGen</MauiXamlInflator>i din.csproj. - NuGet-baseret distribution: MAUI leveres nu som individuelle NuGet-pakker, hvilket giver dig mulighed for at låse specifikke versioner og lettere teste preview-builds.
- CollectionView som standard: De nye handlers for CollectionView og CarouselView er nu standard på iOS og Mac Catalyst. ListView og TableView er deprecated.
- Android API 36-support: Fuld understøttelse af Android 16 med JDK 21.
- .NET Aspire-integration: Ny projektskabelon der forbinder telemetri, service discovery og konfigurationsstyring direkte til dine mobilapps.
- Forbedret MediaPicker: Understøtter nu valg af flere filer, billedkomprimering og automatisk EXIF-håndtering.
Migreringsplan: En faseopdelt tilgang
For større projekter anbefales en faseopdelt migreringstilgang. Her er en praktisk plan, du kan tilpasse til dit eget projekt:
Fase 1: Forberedelse (1-2 uger)
- Gennemfør komplet afhængighedsaudit
- Opdatér Xamarin.Forms til version 5.0 (minimumskrav)
- Opdatér til .NET Standard 2.0 eller højere
- Etablér ydeevne-baselines
- Dokumentér alle custom renderers, effects og DependencyService-implementeringer
Fase 2: Projektstruktur (1 uge)
- Kør .NET Upgrade Assistant
- Konsolidér til single-projekt struktur
- Opdatér alle namespaces
- Erstat NuGet-pakker med MAUI-kompatible versioner
Fase 3: Kodemodernisering (2-4 uger)
- Migrér DependencyService til DI
- Genbrug renderers midlertidigt via compatibility-shims
- Opdatér app-livscyklus og navigation
- Erstat StackLayout med VerticalStackLayout/HorizontalStackLayout
- Tilføj x:DataType for compiled bindings
Fase 4: Handlers og optimering (2-4 uger)
- Migrér kritiske renderers til native handlers
- Migrér ListView/TableView til CollectionView
- Aktivér trimming og AOT i release-builds
- Optimer med XAML Source Generator
Fase 5: Test og lancering (1-2 uger)
- Kør alle enhedstests
- Gennemfør UI-tests på fysiske enheder
- Sammenlign ydeevne med baselines
- Beta-test med udvalgte brugere
- Publicér til app-butikkerne
Konklusion: Migrering er en investering, ikke en byrde
Migrering fra Xamarin.Forms til .NET MAUI er en betydelig opgave — det skal man ikke underdrive. Men det er også en unik mulighed for at modernisere din apps arkitektur, forbedre ydeevnen og sikre fremtidig kompatibilitet med de nyeste platform-krav.
De vigtigste ting at huske:
- Start med en grundig audit — kend dine afhængigheder og renderers, før du begynder
- Brug .NET Upgrade Assistant til det mekaniske arbejde, men forvent at afslutte manuelt
- Migrér i faser — genbrug renderers midlertidigt og konvertér til handlers over tid
- Test grundigt på fysiske enheder og sammenlign med dine ydeevne-baselines
- Udnyt .NET 10's forbedringer — XAML Source Generator, nye CollectionView-handlers og NuGet-distribution
Med den rette planlægning og en struktureret tilgang er migreringen ikke bare mulig — den er en mulighed for at bygge en bedre, hurtigere og mere vedligeholdelsesvenlig app. Og med .NET 10 som LTS-udgivelse har du tre års sikkerhed for patches og opdateringer.
Fremtiden for cross-platform mobiludvikling med .NET er lysere end nogensinde. Så tag springet — du vil ikke fortryde det.