MVVM ใน .NET MAUI ด้วย CommunityToolkit.Mvvm: คู่มือฉบับสมบูรณ์ 2026

คู่มือ MVVM ใน .NET MAUI 2026 ด้วย CommunityToolkit.Mvvm ครบทั้ง ObservableProperty, RelayCommand, DI, Shell navigation, Messenger, Validation และการเขียน unit test พร้อมโค้ดจริงจากโปรดักชัน

MVVM ใน .NET MAUI คู่มือ 2026

อัปเดตล่าสุด: 7 สิงหาคม 2026

MVVM ใน .NET MAUI คือรูปแบบสถาปัตยกรรมที่แยกโค้ด UI (View) ออกจากลอจิก (ViewModel) และข้อมูล (Model) โดยใช้ data binding เป็นสะพานเชื่อม เพื่อให้เทสได้ ปรับ UI ได้อิสระ และดูแลรักษาระยะยาวได้ง่ายกว่า code-behind และในปี 2026 วิธีที่ทีมโปรดักชันใช้กันมากที่สุดคือ CommunityToolkit.Mvvm ซึ่งใช้ Roslyn source generators ตัด boilerplate INotifyPropertyChanged ทิ้งไปเกือบทั้งหมด บทความนี้ผมจะพาดูตั้งแต่การติดตั้ง, [ObservableProperty], [RelayCommand], DI, Shell navigation, Messenger, Validation จนถึงการเขียน unit test โดยใช้โค้ดที่ผมส่งขึ้น production มาแล้วในสามแอปจริง

  • CommunityToolkit.Mvvm 8.4 ใช้ source generators สร้าง property notification ให้อัตโนมัติ ตัดโค้ด OnPropertyChanged ทิ้งได้ทั้งหมด
  • [ObservableProperty] ประกาศบน field แบบ private ตัวเล็ก (_name หรือ name) แล้ว generator จะสร้าง property ตัวใหญ่ให้เอง ใน .NET 9+ ใช้ partial property ได้ตรงๆ
  • [RelayCommand] รองรับ async, CanExecute, และ cancellation token โดยไม่ต้องเขียน ICommand เอง
  • ต้องลงทะเบียน ViewModel และ View ผ่าน MauiAppBuilder.Services เพื่อให้ constructor injection ทำงานผ่าน Shell navigation ได้
  • Shell navigation ส่ง parameter ผ่าน Dictionary<string, object> คู่กับ [QueryProperty] (ห้าม parse route ด้วยมือเอง)
  • ViewModel ที่ผูก UI ควรอยู่ในเทรด UI เสมอ ถ้าอัปเดตจาก background thread ให้ใช้ MainThread.BeginInvokeOnMainThread

MVVM ใน .NET MAUI คืออะไร

MVVM ย่อจาก Model-View-ViewModel ต้นตำรับมาจากทีม WPF ที่ Microsoft เมื่อเกือบ 20 ปีก่อน แนวคิดคือแบ่งความรับผิดชอบเป็นสามส่วน: Model เก็บข้อมูลและ business logic เช่นคลาส DTO หรือ service ที่คุยกับ API, View คือ XAML ที่ผู้ใช้เห็นและกด, ViewModel อยู่ตรงกลาง แปลง Model ให้อยู่ในรูปที่ View ใช้ผูก (bind) ได้ และรับ event จาก View มาแปลงเป็นการเรียก Model

สิ่งที่ทำให้ MVVM ทำงานได้จริงคือ data binding ของ .NET MAUI นั่นคือเมื่อ property ใน ViewModel เปลี่ยน มันจะยิง PropertyChanged event แล้ว binding engine ก็จะสั่ง UI อัปเดตให้เอง ตรงกันข้าม เมื่อผู้ใช้พิมพ์ใน Entry ค่าจะไหลกลับเข้า ViewModel ผ่าน two-way binding

ทำไมถึงต้องแยกกันขนาดนี้? เพราะ ViewModel เทสได้ โดยไม่ต้องรัน UI. คุณเขียน xUnit เรียก method แล้ว assert ค่า property ได้ตรงๆ ต่างจาก code-behind ที่ผูกกับ ContentPage จน mock ไม่ออก และในระยะยาว ถ้า designer จะเปลี่ยน XAML ใหม่ทั้งหน้า ก็ทำได้โดยไม่ต้องแก้ ViewModel เลยแม้แต่บรรทัดเดียว ผมเคยเจอเคสที่ทีม UX ปรับ layout รายเดือน ถ้าไม่ใช้ MVVM คือขุมนรก

ทำไมต้องใช้ CommunityToolkit.Mvvm ไม่เขียน INPC เอง

ถ้าคุณเคยเขียน MVVM แบบ manual ก่อนปี 2022 จะรู้ดีว่า boilerplate เยอะมาก ทุก property ต้องมี backing field, ต้องเรียก SetProperty, ต้อง implement INotifyPropertyChanged ผ่าน base class ยาวเหยียด รวมถึงต้องเขียน ICommand เองด้วย Command class ของ MAUI ซึ่งไม่รองรับ async native

CommunityToolkit.Mvvm (ชื่อเก่า Microsoft.Toolkit.Mvvm) แก้ปัญหาพวกนี้ด้วย Roslyn source generators. คุณเขียน field พร้อม attribute [ObservableProperty] compiler จะสร้าง property, OnPropertyChanged, และ partial method hooks ให้ตอน build ผลคือ ViewModel ที่เคยยาว 200 บรรทัดเหลือ 40 บรรทัด และไม่มี runtime overhead เพิ่ม เพราะ generator สร้างโค้ดจริงลง IL ไม่ได้ใช้ reflection

เวอร์ชันล่าสุดคือ 8.4 (ปล่อยต้นปี 2025) รองรับ partial property ของ C# 13 ที่มากับ .NET 9 หมายความว่าเราไม่ต้องประกาศ field ตัวเล็กแล้ว เขียน partial property ตรงๆ ได้เลย ทำให้ IntelliSense ทำงานเนียนขึ้นมาก

ติดตั้งและตั้งค่า CommunityToolkit.Mvvm ใน .NET MAUI

ก่อนอื่นตรวจสอบว่าใช้ .NET 9 SDK ขึ้นไป (แนะนำ .NET 10 ที่ปล่อยเดือนพฤศจิกายน 2025 เพราะ MAUI ปรับ startup performance เยอะ) จากนั้นเพิ่มแพ็กเกจในโปรเจกต์:

dotnet add package CommunityToolkit.Mvvm --version 8.4.0
dotnet add package Microsoft.Extensions.Hosting --version 9.0.0

เปิด MauiProgram.cs แล้วเตรียม service registration ให้พร้อมสำหรับ ViewModel ที่จะมาต่อ:

using CommunityToolkit.Maui;
using Microsoft.Extensions.Logging;

namespace MyApp;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        // ลงทะเบียน services, ViewModels, และ pages
        builder.Services.AddSingleton<IUserService, UserService>();
        builder.Services.AddTransient<MainViewModel>();
        builder.Services.AddTransient<MainPage>();

#if DEBUG
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}

สังเกตว่าผมลง MainPage เป็น Transient ด้วย นี่คือกุญแจสำคัญ — ถ้าไม่ลง page ผ่าน DI, constructor injection จะไม่ทำงานตอน Shell resolve หน้า และคุณจะได้ null ViewModel ตอน runtime แบบงงๆ

ObservableProperty และ Source Generators

หัวใจของ CommunityToolkit.Mvvm คือ attribute [ObservableProperty] ที่ประกาศบน field ธรรมดา ตัวอย่าง ViewModel สำหรับหน้า login:

using CommunityToolkit.Mvvm.ComponentModel;

public partial class LoginViewModel : ObservableObject
{
    [ObservableProperty]
    private string _username = string.Empty;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(CanLogin))]
    private string _password = string.Empty;

    [ObservableProperty]
    private bool _isBusy;

    public bool CanLogin =>
        !string.IsNullOrWhiteSpace(Username) &&
        !string.IsNullOrWhiteSpace(Password) &&
        !IsBusy;

    partial void OnUsernameChanged(string value)
    {
        // hook เรียกอัตโนมัติทุกครั้งที่ Username เปลี่ยน
        System.Diagnostics.Debug.WriteLine($"Username changed to {value}");
    }
}

สิ่งที่ generator สร้างให้เบื้องหลัง ได้แก่ public property Username, การ raise PropertyChanged, และ partial method hooks (OnUsernameChanging, OnUsernameChanged) ที่คุณ implement เพิ่มได้ นอกจากนี้ [NotifyPropertyChangedFor] จะสั่งให้ยิง notification ของ property ที่ derive มาพร้อมกัน (เหมาะกับ computed properties แบบ CanLogin ที่ต้อง refresh ทุกครั้ง Password เปลี่ยน)

ใน .NET 9 ขึ้นไป คุณสามารถใช้ syntax ใหม่ที่สะอาดกว่าเดิม ไม่ต้องมี field แล้ว:

public partial class LoginViewModel : ObservableObject
{
    [ObservableProperty]
    public partial string Username { get; set; }

    [ObservableProperty]
    public partial string Password { get; set; }
}

ผมยังไม่แนะนำให้ย้ายทั้งโปรเจกต์เป็น partial property ทันที เพราะ IDE tooling บางตัวยังไม่รองรับ refactor เต็มที่ แต่โปรเจกต์ใหม่ให้เริ่มด้วย syntax นี้ได้เลย

RelayCommand: จัดการ Command และ Async อย่างสะอาด

ในโค้ดจริง 80% ของ command ที่ผมเขียนเป็น async ทั้งหมด เพราะต้องเรียก HTTP, database, หรือ SecureStorage. ตรงนี้ [RelayCommand] รองรับเต็มที่ ต่อจาก LoginViewModel:

using CommunityToolkit.Mvvm.Input;

public partial class LoginViewModel : ObservableObject
{
    private readonly IAuthService _authService;

    public LoginViewModel(IAuthService authService)
    {
        _authService = authService;
    }

    [ObservableProperty] private string _username = string.Empty;
    [ObservableProperty] private string _password = string.Empty;
    [ObservableProperty] private bool _isBusy;
    [ObservableProperty] private string? _errorMessage;

    private bool CanLoginExecute() =>
        !string.IsNullOrWhiteSpace(Username) &&
        !string.IsNullOrWhiteSpace(Password) &&
        !IsBusy;

    [RelayCommand(CanExecute = nameof(CanLoginExecute))]
    private async Task LoginAsync(CancellationToken token)
    {
        IsBusy = true;
        ErrorMessage = null;
        try
        {
            var result = await _authService.SignInAsync(Username, Password, token);
            if (result.Success)
                await Shell.Current.GoToAsync("//home");
            else
                ErrorMessage = "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง";
        }
        catch (OperationCanceledException) { /* ผู้ใช้กด cancel */ }
        catch (Exception ex)
        {
            ErrorMessage = $"เกิดข้อผิดพลาด: {ex.Message}";
        }
        finally
        {
            IsBusy = false;
        }
    }
}

Generator จะสร้าง property ชื่อ LoginCommand ให้อัตโนมัติเป็น IAsyncRelayCommand ที่ bind ตรงจาก XAML ได้เลย:

<Button Text="เข้าสู่ระบบ"
        Command="{Binding LoginCommand}"
        IsEnabled="{Binding IsBusy, Converter={StaticResource InvertedBoolConverter}}" />

สังเกตว่าผมรับ CancellationToken เป็น parameter ซึ่ง RelayCommand generator จะสร้าง IncludeCancelCommand ให้ด้วย ทำให้เรามี LoginCancelCommand ฟรีสำหรับผูกปุ่ม Cancel และเมื่อผู้ใช้ navigate ออกจากหน้ากลางคัน generator จะยกเลิก token อัตโนมัติ

ตั้งค่า Dependency Injection สำหรับ ViewModel

ในตัวอย่างข้างบน LoginViewModel รับ IAuthService ผ่าน constructor แต่จะทำงานได้ต่อเมื่อ Shell resolve หน้าและ ViewModel ผ่าน DI container ให้ กลับไปที่ MauiProgram.cs เพิ่ม:

builder.Services.AddSingleton<IAuthService, AuthService>();
builder.Services.AddTransient<LoginViewModel>();
builder.Services.AddTransient<LoginPage>();

แล้วในหน้า LoginPage รับ ViewModel ผ่าน constructor แล้วเซ็ตเข้า BindingContext:

public partial class LoginPage : ContentPage
{
    public LoginPage(LoginViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = viewModel;
    }
}

รูปแบบนี้ทำให้ ViewModel ไม่รู้จัก View เลย เทสยูนิตทดสอบได้โดยแค่ new instance ขึ้นมาแล้วส่ง mock ของ IAuthService เข้าไป ถ้าคุณเคยอ่าน คู่มือ .NET MAUI SQLite แบบ offline-first ของผมมาก่อน จะเห็นว่าใช้รูปแบบ DI เดียวกันทุกที่. พอทำได้แล้วมันสม่ำเสมอทั้ง codebase

ส่ง Parameter ผ่าน Shell Navigation อย่างไร

คำถามที่เจอบ่อยที่สุดจากทีมที่ย้ายมาจาก Xamarin.Forms คือ "จะส่งค่าไปหน้าถัดไปยังไง". คำตอบใน MAUI คือ Shell navigation พร้อม Dictionary<string, object> ตัวอย่างส่ง userId ไปหน้าโปรไฟล์:

// ในหน้าปัจจุบัน
await Shell.Current.GoToAsync($"profile", new Dictionary<string, object>
{
    ["UserId"] = 42,
    ["Source"] = "search-results"
});

ปลายทาง ProfileViewModel ประกาศ property ที่จะรับด้วย [QueryProperty]:

[QueryProperty(nameof(UserId), "UserId")]
[QueryProperty(nameof(Source), "Source")]
public partial class ProfileViewModel : ObservableObject
{
    [ObservableProperty] private int _userId;
    [ObservableProperty] private string _source = string.Empty;

    partial void OnUserIdChanged(int value)
    {
        // เมื่อ Shell เซ็ตค่า UserId เข้ามา ให้โหลด data
        _ = LoadProfileAsync();
    }

    private async Task LoadProfileAsync() { /* ... */ }
}

อย่าลืม register route ใน AppShell.xaml.cs:

public AppShell()
{
    InitializeComponent();
    Routing.RegisterRoute("profile", typeof(ProfilePage));
}

ข้อผิดพลาดที่ผมเห็นบ่อยคือทีมพยายามใส่ค่าเป็น query string แบบ ?UserId=42 แล้วปวดหัวกับการ encode object ที่ซับซ้อน — ใช้ Dictionary เถอะ MAUI จะเก็บ reference จริงให้ปลายทางเลย ไม่ต้อง serialize

Messenger: สื่อสารระหว่าง ViewModel โดยไม่ผูกกันตรงๆ

เมื่อคุณต้องแจ้ง ViewModel อื่นว่ามีอะไรเปลี่ยน (เช่น หน้า cart ต้องรู้ว่าหน้า product เพิ่งเพิ่มของลงตะกร้า) การส่ง reference กันตรงๆ จะทำให้ ViewModel ผูกกันแน่นและเทสยาก CommunityToolkit.Mvvm มี WeakReferenceMessenger ให้ใช้ pub/sub แบบสะอาด

// สร้าง message class
public record ItemAddedToCartMessage(int ProductId, int Quantity);

// ผู้ส่ง: ProductViewModel
[RelayCommand]
private void AddToCart()
{
    WeakReferenceMessenger.Default.Send(new ItemAddedToCartMessage(Product.Id, 1));
}

// ผู้รับ: CartViewModel
public CartViewModel()
{
    WeakReferenceMessenger.Default.Register<ItemAddedToCartMessage>(this, (r, m) =>
    {
        // อัปเดต cart count
        ItemCount++;
    });
}

ใช้ WeakReference เป็น default เพราะไม่ต้องกังวลเรื่อง memory leak (ถ้า ViewModel ถูก garbage collect ระบบจะเลิก subscription อัตโนมัติ) ถ้าอยากรู้วิธีป้องกัน memory leak รูปแบบอื่นๆ ผมเขียนรายละเอียดไว้ใน คู่มือปรับ performance ของ .NET MAUI แล้ว

Validation ด้วย ObservableValidator

สำหรับ form ที่ต้อง validate input CommunityToolkit.Mvvm มี base class ObservableValidator ที่ผูกกับ System.ComponentModel.DataAnnotations:

using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;

public partial class RegisterViewModel : ObservableValidator
{
    [ObservableProperty]
    [Required(ErrorMessage = "กรุณากรอกอีเมล")]
    [EmailAddress(ErrorMessage = "รูปแบบอีเมลไม่ถูกต้อง")]
    [NotifyDataErrorInfo]
    private string _email = string.Empty;

    [ObservableProperty]
    [Required(ErrorMessage = "กรุณากรอกรหัสผ่าน")]
    [MinLength(8, ErrorMessage = "รหัสผ่านต้องยาวอย่างน้อย 8 ตัวอักษร")]
    [NotifyDataErrorInfo]
    private string _password = string.Empty;

    [RelayCommand]
    private void Register()
    {
        ValidateAllProperties();
        if (HasErrors) return;
        // ...
    }
}

Attribute [NotifyDataErrorInfo] จะสั่งให้ generator เรียก ValidateProperty ทุกครั้ง property เปลี่ยน MAUI จะดึง error message ผ่าน INotifyDataErrorInfo ไปแสดงใน ValidationVisualState ของ Entry ให้อัตโนมัติ

เขียน Unit Test สำหรับ ViewModel

ประโยชน์ใหญ่ที่สุดของ MVVM คือ ViewModel เทสได้โดยไม่ต้องรัน emulator ผมใช้ xUnit + NSubstitute ตัวอย่างเทส LoginViewModel ข้างบน:

using NSubstitute;
using Xunit;

public class LoginViewModelTests
{
    [Fact]
    public async Task LoginAsync_WhenCredentialsInvalid_SetsErrorMessage()
    {
        // Arrange
        var authService = Substitute.For<IAuthService>();
        authService.SignInAsync("wrong", "wrong", Arg.Any<CancellationToken>())
            .Returns(new AuthResult(Success: false));

        var vm = new LoginViewModel(authService)
        {
            Username = "wrong",
            Password = "wrong"
        };

        // Act
        await vm.LoginCommand.ExecuteAsync(null);

        // Assert
        Assert.Equal("ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง", vm.ErrorMessage);
        Assert.False(vm.IsBusy);
    }

    [Fact]
    public void CanLogin_FalseWhenBusy()
    {
        var vm = new LoginViewModel(Substitute.For<IAuthService>())
        {
            Username = "user",
            Password = "pass",
            IsBusy = true
        };
        Assert.False(vm.LoginCommand.CanExecute(null));
    }
}

คีย์คือเราสร้าง ViewModel เป็น instance ธรรมดา ไม่ต้องแตะ ContentPage เลย ถ้าคุณเพิ่งย้ายจาก Xamarin.Forms มาและอยากเข้าใจ pattern ที่ผมใช้ต่อยอด ลองอ่าน คู่มือย้ายจาก Xamarin.Forms ไป .NET MAUI ประกอบ เพราะ MVVM ที่ทำในนั้นใช้แนวเดียวกันนี้ทั้งหมด

ข้อผิดพลาดที่พบบ่อยและวิธีหลีกเลี่ยง

ผมสรุปข้อผิดพลาดที่ผมเห็นซ้ำๆ ใน code review ตลอดสามปีที่ผ่านมา. ทั้งหมดนี้เป็นเคสจริงที่ทำให้ทีมเสียเวลา debug เป็นวันๆ ผมจึงอยากรวบไว้ในที่เดียวเพื่อให้ทีมของคุณข้ามพลาดตรงนี้ไปได้เลย:

1. อัปเดต ObservableCollection จาก background thread

MAUI จะ throw exception ถ้าคุณ add/remove items จากเทรดอื่นที่ไม่ใช่ UI thread เพราะ CollectionView ต้องอัปเดต visual tree ซึ่งทำได้เฉพาะบน main thread เท่านั้น วิธีที่ถูกคือครอบด้วย MainThread.InvokeOnMainThreadAsync ทุกครั้งที่โหลด data มาจาก HTTP หรือ database:

await MainThread.InvokeOnMainThreadAsync(() => Items.Add(newItem));

ทางเลือกที่ผมชอบกว่าคือใช้ ObservableCollection เป็น temporary แล้ว replace reference ทีเดียวเมื่อโหลดเสร็จ วิธีนี้ยิง PropertyChanged ครั้งเดียวเลย ประหยัด layout pass ได้เยอะ

2. Subscribe messenger โดยไม่ unregister

WeakReferenceMessenger ช่วยได้ระดับหนึ่ง แต่ถ้าใช้ StrongReferenceMessenger ต้องเรียก UnregisterAll ใน OnDisappearing ทุกครั้ง ไม่งั้น ViewModel จะไม่ถูก GC และ event handler จะสะสมจนแอปหน่วง ผมเคยเจอเคสที่ผู้ใช้เปิด-ปิดหน้าเดิม 20 รอบแล้วแอปช้าจน UI ค้าง หาต้นตอคือ subscription ที่ค้างอยู่ 20 subscription ยิง event เดียวกันพร้อมกัน

3. ใช้ Property Setter ทำงานหนัก

อย่าใส่ HTTP call ใน OnUsernameChanged เพราะทุกตัวอักษรที่ผู้ใช้พิมพ์จะยิง request ถ้าต้องทำ search-as-you-type ให้ debounce ด้วย CancellationTokenSource ที่ยกเลิกของก่อนหน้าก่อนยิงใหม่ หรือ throttle 300–500ms ด้วย Task.Delay ก่อนเช็ก token

4. ผูก ViewModel กับ ContentPage โดยตรง

ถ้า ViewModel using Microsoft.Maui.Controls คุณเทสยากทันที เพราะ test project ต้องรัน MAUI runtime เต็มรูปแบบ เก็บทุกสิ่งที่ผูกกับ UI ไว้ในหน้า และให้ ViewModel รู้จักแค่ interface ที่คุณ define เอง เช่น INavigationService, IDialogService, IToastService วิธีนี้ mock ง่ายและ swap implementation ระหว่าง unit test กับ production ได้อิสระ

5. ประกาศ ObservableProperty เป็น public field

Generator ต้องการ private field เท่านั้น ถ้าประกาศ public compiler จะเงียบและ property จะไม่ถูกสร้าง. เช็กด้วยการดูใน obj/Generated ว่ามีไฟล์ generated ออกมาไหม หรือเปิด "Go to Definition" บน property ที่คิดว่า generator สร้างให้ ถ้าเปิดไปที่ field แสดงว่า generator ไม่ทำงาน ตรวจ visibility ก่อนเป็นอันดับแรก

6. Bind หลาย property จาก computed getter ที่หนัก

ถ้ามี computed property ที่คำนวณจาก LINQ หรือ regex ทุกครั้งที่ getter ถูกเรียก UI จะ trigger การคำนวณซ้ำๆ ตอน layout ทำ cache ผลลัพธ์ไว้ใน field แล้ว invalidate ผ่าน [NotifyPropertyChangedFor] เท่าที่จำเป็น

คำถามที่พบบ่อย

CommunityToolkit.Mvvm ทำงานร่วมกับ .NET MAUI 10 ได้ไหม?

ได้ทั้งหมด. CommunityToolkit.Mvvm 8.4 เป็น pure .NET Standard 2.0 library ไม่ผูกกับ MAUI โดยตรง จึงใช้ได้กับ .NET MAUI 8, 9, และ 10 ที่ปล่อยเดือนพฤศจิกายน 2025 โดยไม่ต้องแก้โค้ด

ต่างกันอย่างไรกับ ReactiveUI ใน .NET MAUI?

CommunityToolkit.Mvvm เน้น boilerplate น้อยและใช้ Roslyn source generators ส่วน ReactiveUI ใช้ Rx.NET เน้น functional reactive programming ถ้าทีมยังไม่คุ้น Rx แนะนำเริ่มจาก CommunityToolkit.Mvvm ก่อน เพราะ learning curve เบากว่า และครอบคลุม 90% ของ use case ในแอปโปรดักชัน

ใช้ [ObservableProperty] กับ record หรือ struct ได้ไหม?

ไม่ได้ เพราะ generator สร้าง partial method และแก้ property setter ซึ่งต้องการ mutable reference type ถ้าอยากใช้กับ record ให้ประกาศ property manual แล้วเรียก SetProperty ใน setter หรือใช้ ObservableObject ครอบเป็น wrapper

RelayCommand รองรับ async/await จริงหรือ?

รองรับเต็มที่ ถ้า method return Task, generator จะสร้าง IAsyncRelayCommand ให้อัตโนมัติ พร้อม property IsRunning ที่ bind ใน XAML ได้เพื่อทำ loading indicator นอกจากนี้ถ้ารับ CancellationToken เป็น parameter จะได้ CancelCommand ฟรีสำหรับปุ่มยกเลิก

ต้องเปลี่ยน XAML เมื่อย้ายจาก manual MVVM มาใช้ CommunityToolkit.Mvvm หรือไม่?

ไม่ต้อง เพราะ binding path (เช่น {Binding Username}) อ้างอิงชื่อ public property ซึ่ง generator สร้างให้ตรงกับที่คุณเคยเขียน manual ทำให้ XAML เดิมใช้ได้ทันทีหลังย้าย โดยแทบไม่ต้องแก้อะไรเลยนอกจาก base class ของ ViewModel

Marcus Chen
เกี่ยวกับผู้เขียน Marcus Chen

Senior mobile architect with a decade of cross-platform experience. Spent the last five years going deep on .NET MAUI in production.