- Mỗi lần gọi platform code trong .NET MAUI mà phải nhồi #if ANDROID / #if IOS khắp nơi — code khó đọc, IntelliSense rối, refactor như đi mìn.
- Có một cách sạch hơn nhiều: partial class kết hợp filename suffix .Android.cs / .iOS.cs.
- Build system MAUI tự multi-target, mỗi platform một file riêng, zero conditional compilation.
- Bài này giải thích pattern, code mẫu, và cấu hình .csproj đầy đủ.
TL;DR
Thay vì nhồi #if ANDROID / #if IOS khắp một service, bạn có thể tách code platform-specific ra file riêng theo suffix tên file (MyService.Android.cs, MyService.iOS.cs) và để chúng cùng là partial class với file shared MyService.cs. Build system của .NET MAUI multi-target sẽ chỉ compile đúng file cho TFM đang build. Kết quả: zero conditional compilation, IntelliSense đầy đủ cho từng platform, file ngắn, refactor an toàn.
Vì sao #if trở thành rác
Cách phổ biến nhất khi cần gọi platform API trong shared code là conditional compilation. Microsoft Learn đưa ngay ví dụ này:
#if ANDROID
IWindowManager windowManager = Android.App.Application.Context.GetSystemService(Context.WindowService).JavaCast<IWindowManager>();
SurfaceOrientation orientation = windowManager.DefaultDisplay.Rotation;
bool isLandscape = orientation == SurfaceOrientation.Rotation90 || orientation == SurfaceOrientation.Rotation270;
return isLandscape ? DeviceOrientation.Landscape : DeviceOrientation.Portrait;
#elif IOS
UIInterfaceOrientation orientation = UIApplication.SharedApplication.StatusBarOrientation;
bool isPortrait = orientation == UIInterfaceOrientation.Portrait || orientation == UIInterfaceOrientation.PortraitUpsideDown;
return isPortrait ? DeviceOrientation.Portrait : DeviceOrientation.Landscape;
#else
return DeviceOrientation.Undefined;
#endifMột method ngắn còn ổn. Nhưng khi service phình lên 5–10 method, mỗi method 3 nhánh platform, file biến thành mê cung. IntelliSense chỉ active đúng 1 nhánh tại một thời điểm — sửa nhánh Android xong push đi, nhánh iOS có thể đã sai cú pháp mà bạn không hề biết cho tới khi CI build iOS.
Partial class: cùng tên, cùng namespace, khác file
Pattern thay thế dựa trên tính năng partial class + partial method của C#. Bạn khai báo signature ở file shared, implement ở file platform — và compiler ghép chúng lại như cùng một class.
File shared (Services/DeviceOrientationService.cs, để ngoài thư mục Platforms/):
namespace InvokePlatformCodeDemos.Services.PartialMethods;
public partial class DeviceOrientationService
{
public partial DeviceOrientation GetOrientation();
}File Android (Services/DeviceOrientationService.Android.cs):
using Android.Content;
using Android.Runtime;
using Android.Views;
namespace InvokePlatformCodeDemos.Services.PartialMethods;
public partial class DeviceOrientationService
{
public partial DeviceOrientation GetOrientation()
{
IWindowManager windowManager = Android.App.Application.Context.GetSystemService(Context.WindowService).JavaCast<IWindowManager>();
SurfaceOrientation orientation = windowManager.DefaultDisplay.Rotation;
bool isLandscape = orientation == SurfaceOrientation.Rotation90 || orientation == SurfaceOrientation.Rotation270;
return isLandscape ? DeviceOrientation.Landscape : DeviceOrientation.Portrait;
}
}File iOS (Services/DeviceOrientationService.iOS.cs):
using UIKit;
namespace InvokePlatformCodeDemos.Services.PartialMethods;
public partial class DeviceOrientationService
{
public partial DeviceOrientation GetOrientation()
{
UIInterfaceOrientation orientation = UIApplication.SharedApplication.StatusBarOrientation;
bool isPortrait = orientation == UIInterfaceOrientation.Portrait || orientation == UIInterfaceOrientation.PortraitUpsideDown;
return isPortrait ? DeviceOrientation.Portrait : DeviceOrientation.Landscape;
}
}Phía consumer chỉ thấy new DeviceOrientationService().GetOrientation() — không cần biết platform. Mỗi file platform có IntelliSense riêng cho native SDK của nó (UIKit, Android.Views) — full type-checking, không có vùng code "mờ" như #if.
Hai cách multi-target: folder vs filename
.NET MAUI cho bạn chọn 1 trong 2 (hoặc kết hợp):
1. Folder-based (mặc định, không cần config)
Đặt file vào đúng Platforms/Android/, Platforms/iOS/, ... Build system tự loại trừ folder không match TFM. Hợp với app — code platform thường ít, gom 1 chỗ tiện.
2. Filename-based (suffix .Platform.cs — cần config csproj)
File nằm cạnh code shared, đặt tên kết thúc bằng .Android.cs, .iOS.cs... — discoverability cao, code platform nằm sát logic chung. Đây là cách Gerald Versluis (MAUI PM) khuyến nghị cho service lớn và library tác giả.
Pattern này không được wire-up sẵn — bạn phải thêm vào .csproj:
<!-- Android -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-android')) != true">
<Compile Remove="**\*.Android.cs" />
<None Include="**\*.Android.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>
<!-- iOS -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-ios')) != true">
<Compile Remove="**\*.iOS.cs" />
<None Include="**\*.iOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>
<!-- MacCatalyst -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-maccatalyst')) != true">
<Compile Remove="**\*.MacCatalyst.cs" />
<None Include="**\*.MacCatalyst.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>
<!-- Windows -->
<ItemGroup Condition="$(TargetFramework.Contains('-windows')) != true">
<Compile Remove="**\*.Windows.cs" />
<None Include="**\*.Windows.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>Có thêm .MaciOS.cs nếu muốn 1 file dùng chung cho cả iOS và MacCatalyst. Bảng đầy đủ:
| Suffix | Compile khi TFM |
|---|---|
.Android.cs | bắt đầu bằng net8.0-android |
.iOS.cs | bắt đầu bằng net8.0-ios |
.MacCatalyst.cs | bắt đầu bằng net8.0-maccatalyst |
.MaciOS.cs | iOS hoặc MacCatalyst |
.Windows.cs | chứa -windows |
Khi nào dùng cái nào
- Vẫn dùng #if cho one-liner thực sự ngắn (1–2 dòng khác biệt), hoặc khi cần fallback graceful (
return Undefinedtrên platform chưa support). - Dùng partial class + folder cho app size vừa, code platform tập trung dưới
Platforms/. - Dùng partial class + filename suffix cho service lớn, library NuGet, hoặc khi muốn code platform "đứng cạnh" logic shared để dễ navigate.
Lưu ý quan trọng
- Mọi part phải cùng namespace, cùng class name. Sai 1 ký tự — không ghép được, build fail.
- Bạn phải implement
partialmethod trên mọi TFM khai báo trong<TargetFrameworks>. Quên 1 platform = build platform đó fail. Đây là trade-off so với#if: mất khả năng "skip nhẹ nhàng". partialmethod có return value bắt buộc phải có implementation (C# spec).- Khi thêm TFM mới (ví dụ Tizen) — đừng quên cập nhật ItemGroup trong
.csprojnếu xài filename suffix. - Zero runtime cost — toàn bộ multi-targeting xử lý ở compile-time.
Kết
Với một service nhỏ, #if tạm ổn. Nhưng ngay khi class chạm 100+ dòng hoặc bạn ship library cho người khác xài, partial class + filename suffix là chuẩn industry mà MAUI team khuyến nghị. Code đọc dễ hơn, IDE thông minh hơn, regression giảm hẳn.
Nguồn: Microsoft Learn — Invoking platform code, Configure MAUI multi-targeting, tip gốc của Gerald Versluis.
