Úvod: Proč potřebujete MVVM a proč právě CommunityToolkit.Mvvm
Pokud vyvíjíte mobilní aplikaci v .NET MAUI, dříve nebo později narazíte na tu samou otázku: jak vlastně správně strukturovat kód, aby šel testovat, udržovat a aby se v něm vůbec někdo vyznal? Odpovědí je architektonický vzor Model-View-ViewModel (MVVM). A upřímně — nejlepším nástrojem pro jeho implementaci v .NET MAUI je dnes balíček CommunityToolkit.Mvvm.
Tenhle balíček je oficiálně spravovaný Microsoftem jako součást .NET Foundation a dokáže eliminovat obrovské množství opakujícího se kódu díky source generátorům založeným na Roslynu. Místo desítek řádků boilerplate kódu pro každou vlastnost a příkaz napíšete jeden atribut. A co je nejdůležitější — vygenerovaný kód je plně kompatibilní s NativeAOT a trimmingem. Žádné nepříjemné překvapení v produkčních buildech.
Tak pojďme na to. V tomhle průvodci projdeme CommunityToolkit.Mvvm od úplných základů až po pokročilé scénáře. Každou část doprovodíme funkčními příklady kódu, které můžete rovnou hodit do svého projektu. Pokryjeme aktuální verzi 8.4, která přináší podporu partial properties z C# 14 — a to je zásadní novinka, která mění způsob práce s [ObservableProperty].
Instalace a základní nastavení
Začneme tím nejjednodušším — instalací. Balíček je na NuGetu a nepotřebuje žádné další závislosti:
dotnet add package CommunityToolkit.Mvvm
Pro .NET MAUI projekty doporučuju verzi 8.4.2 nebo novější, která plně podporuje .NET 10, C# 14 partial properties a NativeAOT kompilaci. Po instalaci není potřeba nic dalšího konfigurovat — source generátory se aktivují automaticky při kompilaci.
Jeden důležitý detail, na který se občas zapomíná: všechny třídy využívající source generátory musí být označeny jako partial. Generátor totiž vytváří doplňkový kód ve druhé části parciální třídy. Vynecháte-li partial, kompilátor vás na to upozorní chybou.
ObservableObject: Základní kámen ViewModelu
Každý ViewModel ve vaší aplikaci by měl dědit z třídy ObservableObject. Ta implementuje rozhraní INotifyPropertyChanged a INotifyPropertyChanging — klíčové kontrakty pro data binding v .NET MAUI.
using CommunityToolkit.Mvvm.ComponentModel;
public partial class MainViewModel : ObservableObject
{
// Zde budou vaše vlastnosti a příkazy
}
ObservableObject nabízí metodu SetProperty, kterou můžete použít pro ruční implementaci vlastností s notifikací. Ale ruku na srdce — ve většině případů budete raději používat atribut [ObservableProperty], který celou implementaci vygeneruje za vás.
Kdy použít ObservableValidator místo ObservableObject
Pokud váš ViewModel potřebuje validaci vstupních dat (typicky ve formulářích), sáhněte po třídě ObservableValidator. Ta rozšiřuje ObservableObject o podporu validačních atributů z System.ComponentModel.DataAnnotations:
public partial class RegistrationViewModel : ObservableValidator
{
[ObservableProperty]
[Required(ErrorMessage = "Email je povinný")]
[EmailAddress(ErrorMessage = "Neplatný formát emailu")]
private string _email = string.Empty;
[ObservableProperty]
[MinLength(8, ErrorMessage = "Heslo musí mít alespoň 8 znaků")]
private string _password = string.Empty;
[RelayCommand]
private void Submit()
{
ValidateAllProperties();
if (HasErrors)
return;
// Zpracování formuláře
}
}
Atribut [ObservableProperty]: Konec s boilerplate kódem
Tohle je srdce celého toolkitu. A vlastně i důvod, proč ho tolik lidí používá. Místo psaní desítek řádků pro každou property stačí jediný atribut nad privátním polem.
Tradiční přístup (bez toolkitu)
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
OnPropertyChanging(nameof(Name));
_name = value;
OnPropertyChanged(nameof(Name));
}
}
}
Upřímně, tohle psát ručně pro každou vlastnost je šílené. Podívejte se, jak to samé vypadá s toolkitem:
S CommunityToolkit.Mvvm
public partial class ProfileViewModel : ObservableObject
{
[ObservableProperty]
private string _name = string.Empty;
[ObservableProperty]
private int _age;
[ObservableProperty]
private bool _isActive;
}
Source generátor automaticky vytvoří veřejné vlastnosti Name, Age a IsActive s kompletní implementací INotifyPropertyChanged. Konvence je jednoduchá: privátní pole _name nebo name se transformuje na veřejnou vlastnost Name.
Háčky na změnu hodnoty: OnChanging a OnChanged
Pro každou vlastnost generátor vytváří dvojici parciálních metod, do kterých můžete vložit svoji logiku. Tohle je nesmírně praktické:
public partial class ProfileViewModel : ObservableObject
{
[ObservableProperty]
private string _name = string.Empty;
// Volá se PŘED nastavením nové hodnoty
partial void OnNameChanging(string value)
{
Debug.WriteLine($"Jméno se mění z '{Name}' na '{value}'");
}
// Volá se PO nastavení nové hodnoty
partial void OnNameChanged(string value)
{
Debug.WriteLine($"Jméno bylo změněno na '{value}'");
}
}
Tyhle háčky jsou skvělé pro vedlejší efekty — logování, aktualizaci závislých hodnot nebo spouštění dalších operací při změně stavu. Osobně je používám nejčastěji pro reaktivní filtrování a validaci.
Notifikace závislých vlastností: NotifyPropertyChangedFor
Často potřebujete vlastnosti, které závisí na jiných. Klasický příklad: FullName závisí na FirstName a LastName. Atribut [NotifyPropertyChangedFor] zajistí, že při změně jedné vlastnosti se pošle notifikace i pro tu závislou:
public partial class PersonViewModel : ObservableObject
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string _firstName = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string _lastName = string.Empty;
public string FullName => $"{FirstName} {LastName}";
}
Partial properties v C# 14: Nový způsob práce s [ObservableProperty]
Verze 8.4 CommunityToolkit.Mvvm přinesla zásadní novinku — podporu partial properties. Díky C# 14 (dostupnému v .NET 10 SDK) už nemusíte deklarovat privátní pole. Místo toho přímo definujete parciální vlastnost:
public partial class ModernViewModel : ObservableObject
{
// Nový přístup s partial properties (C# 14+)
[ObservableProperty]
public partial string Title { get; set; }
[ObservableProperty]
public partial int Count { get; set; }
[ObservableProperty]
public partial bool IsLoading { get; set; }
}
Tohle je budoucnost. Oproti starému přístupu s privátními poli tu máte hned několik výhod:
- Plná integrace s jazykem — fungují všechny C# modifikátory jako
required,override,sealednebonew - Lepší nullability anotace — kompilátor správně rozumí nullable kontextu
- Vlastní přístupové modifikátory — můžete mít
public getaprivate set - Kompatibilita s NativeAOT — plná podpora pro AOT kompilaci
- Lepší čitelnost — kód vypadá jako standardní C# vlastnost
public partial class AdvancedViewModel : ObservableObject
{
// Vlastnost s private setterem
[ObservableProperty]
public partial string Id { get; private set; }
// Required vlastnost
[ObservableProperty]
public required partial string Name { get; set; }
}
Tip: Pokud používáte .NET 10 SDK a CommunityToolkit.Mvvm 8.4+, určitě migrujte na partial properties. Starý přístup s privátními poli pořád funguje, ale partial properties jsou jednoznačně směr, kterým se to celé ubírá.
Příkazy s [RelayCommand]: Synchronní i asynchronní
Příkazy jsou v MVVM vzoru mostem mezi UI a logikou ViewModelu. Atribut [RelayCommand] vám vygeneruje kompletní implementaci příkazu z jednoduché metody. Žádné ruční vytváření ICommand instancí v konstruktoru.
Synchronní příkazy
public partial class CounterViewModel : ObservableObject
{
[ObservableProperty]
private int _count;
[RelayCommand]
private void Increment()
{
Count++;
}
[RelayCommand]
private void Reset()
{
Count = 0;
}
}
Generátor vytvoří vlastnosti IncrementCommand a ResetCommand typu IRelayCommand. Konvence je prostá — k názvu metody se přidá přípona Command.
Příkazy s parametrem
[RelayCommand]
private void AddItem(string item)
{
Items.Add(item);
}
// V XAML: Command="{Binding AddItemCommand}"
// CommandParameter="{Binding NewItemText}"
Asynchronní příkazy
A tady toolkit skutečně září. AsyncRelayCommand přináší věci, které byste jinak museli řešit ručně:
public partial class DataViewModel : ObservableObject
{
private readonly IApiService _apiService;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private ObservableCollection<Product> _products = new();
public DataViewModel(IApiService apiService)
{
_apiService = apiService;
}
[RelayCommand]
private async Task LoadProductsAsync()
{
IsLoading = true;
try
{
var result = await _apiService.GetProductsAsync();
Products = new ObservableCollection<Product>(result);
}
finally
{
IsLoading = false;
}
}
}
Klíčový detail: AsyncRelayCommand ve výchozím nastavení neumožňuje souběžné spuštění. Klepne-li uživatel na tlačítko dvakrát rychle za sebou, druhý klik se prostě ignoruje, dokud první operace neskončí. Tohle je obrovská výhoda — žádné duplicitní API volání, žádné race conditions. Na tohle jsem si sám párkrát naběhl, než jsem začal toolkit používat.
Asynchronní příkazy s CancellationToken
Pokud vaše metoda přijímá CancellationToken, generátor automaticky vytvoří příkaz s podporou zrušení. Uživatel tak může stahování nebo jinou dlouhou operaci přerušit:
[RelayCommand]
private async Task DownloadFileAsync(string url, CancellationToken ct)
{
IsDownloading = true;
try
{
var data = await _httpClient.GetByteArrayAsync(url, ct);
await File.WriteAllBytesAsync(
GetLocalPath(url), data, ct);
}
catch (OperationCanceledException)
{
// Uživatel zrušil stahování
}
finally
{
IsDownloading = false;
}
}
// V XAML můžete nabindovat i zrušení:
// <Button Text="Zrušit"
// Command="{Binding DownloadFileCommand.CancelCommand}" />
CanExecute: Řízení dostupnosti příkazů
Příkazy v MVVM mohou mít podmínku spustitelnosti — CanExecute. Toolkit nabízí elegantní způsob, jak tuhle podmínku propojit s vlastnostmi ViewModelu:
public partial class LoginViewModel : ObservableObject
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(LoginCommand))]
private string _username = string.Empty;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(LoginCommand))]
private string _password = string.Empty;
[RelayCommand(CanExecute = nameof(CanLogin))]
private async Task LoginAsync()
{
// Přihlášení uživatele
await _authService.LoginAsync(Username, Password);
}
private bool CanLogin()
{
return !string.IsNullOrWhiteSpace(Username)
&& !string.IsNullOrWhiteSpace(Password);
}
}
Jak to funguje krok za krokem:
- Uživatel zadá text do pole pro uživatelské jméno
- Změní se vlastnost
Username - Díky
[NotifyCanExecuteChangedFor]se automaticky zavoláLoginCommand.NotifyCanExecuteChanged() - Tlačítko napojené na
LoginCommandznovu vyhodnotí metoduCanLogin() - Vrátí-li podmínka
true, tlačítko se aktivuje; jinak zůstane neaktivní
Výsledek? Tlačítko přihlášení je automaticky zakázané, dokud uživatel nevyplní obě pole. Žádný manuální kód pro řízení stavu tlačítka — prostě to funguje.
Messaging: Komunikace mezi ViewModely
V reálných aplikacích potřebujete, aby spolu ViewModely komunikovaly — ale bez přímých referencí. To by porušilo celý smysl MVVM. CommunityToolkit.Mvvm na to má WeakReferenceMessenger — systém volně vázané komunikace založený na zprávách.
Definice zprávy
// Jednoduchá zpráva s hodnotou
public class ProductAddedMessage : ValueChangedMessage<Product>
{
public ProductAddedMessage(Product product) : base(product)
{
}
}
// Zpráva pro požadavek-odpověď
public class GetCurrentUserMessage
: RequestMessage<User>
{
}
Odeslání zprávy
public partial class AddProductViewModel : ObservableObject
{
[RelayCommand]
private async Task SaveProductAsync()
{
var product = new Product
{
Name = Name,
Price = Price
};
await _productService.CreateAsync(product);
// Odeslání zprávy ostatním ViewModelům
WeakReferenceMessenger.Default.Send(
new ProductAddedMessage(product));
await Shell.Current.GoToAsync("..");
}
}
Příjem zprávy
public partial class ProductListViewModel : ObservableObject,
IRecipient<ProductAddedMessage>
{
public ProductListViewModel()
{
// Registrace pro příjem zpráv
WeakReferenceMessenger.Default.Register(this);
}
public void Receive(ProductAddedMessage message)
{
var newProduct = message.Value;
Products.Add(newProduct);
}
}
Proč WeakReferenceMessenger a ne StrongReferenceMessenger? Výchozí WeakReferenceMessenger používá slabé reference, takže nebrání garbage collectoru v uvolnění příjemce. Pro mobilní aplikace je to bezpečnější volba — správa paměti tam hraje klíčovou roli. StrongReferenceMessenger je sice o něco rychlejší, ale vyžaduje explicitní odregistraci. A pokud na ni zapomenete? Memory leak.
Integrace s Dependency Injection v .NET MAUI
CommunityToolkit.Mvvm se skvěle integruje s vestavěným DI kontejnerem v .NET MAUI. Tady je správný postup, jak to celé propojit.
Registrace v MauiProgram.cs
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Registrace služeb
builder.Services.AddSingleton<IApiService, ApiService>();
builder.Services.AddSingleton<IAuthService, AuthService>();
builder.Services.AddSingleton<IProductService, ProductService>();
// Registrace ViewModelů jako Transient
builder.Services.AddTransient<ProductListViewModel>();
builder.Services.AddTransient<ProductDetailViewModel>();
builder.Services.AddTransient<LoginViewModel>();
// Registrace stránek jako Transient
builder.Services.AddTransient<ProductListPage>();
builder.Services.AddTransient<ProductDetailPage>();
builder.Services.AddTransient<LoginPage>();
return builder.Build();
}
}
Injektování závislostí do ViewModelu
public partial class ProductListViewModel : ObservableObject
{
private readonly IProductService _productService;
private readonly INavigationService _navigationService;
[ObservableProperty]
private ObservableCollection<Product> _products = new();
[ObservableProperty]
private bool _isRefreshing;
public ProductListViewModel(
IProductService productService)
{
_productService = productService;
}
[RelayCommand]
private async Task LoadDataAsync()
{
try
{
var items = await _productService.GetAllAsync();
Products = new ObservableCollection<Product>(items);
}
finally
{
IsRefreshing = false;
}
}
}
Propojení ViewModelu se stránkou
public partial class ProductListPage : ContentPage
{
public ProductListPage(ProductListViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}
DI kontejner v .NET MAUI automaticky vyřeší všechny závislosti. Když Shell naviguje na ProductListPage, kontejner vytvoří instanci stránky, pro ni vyrobí ProductListViewModel, do kterého injektuje IProductService. Celý řetězec závislostí se vyřeší sám — a to je přesně ta magie, pro kterou DI používáme.
Singleton vs. Transient: Správná volba životnosti
U mobilních aplikací je volba životnosti registrace důležitější než u webových (kde máte přirozené request scope). Tady jsou základní pravidla:
- Singleton — pro služby sdílené celou aplikací (HttpClient wrappery, nastavení, cache). Žije po celou dobu běhu aplikace.
- Transient — pro ViewModely a stránky. Každá navigace vytvoří novou instanci, což zajistí čistý stav bez artefaktů z předchozí návštěvy.
- Scoped — v .NET MAUI (mimo Blazor Hybrid) nemá přirozené scope hranice. Používejte jen pokud opravdu víte, co děláte.
Kompletní příklad: ViewModel pro správu úkolů
Pojďme teď spojit všechno dohromady. Tady je realistický příklad — ViewModel pro jednoduchou správu úkolů:
public partial class TodoViewModel : ObservableObject
{
private readonly ITodoService _todoService;
[ObservableProperty]
private ObservableCollection<TodoItem> _todos = new();
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(AddTodoCommand))]
private string _newTodoText = string.Empty;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _filterText = string.Empty;
public TodoViewModel(ITodoService todoService)
{
_todoService = todoService;
}
[RelayCommand]
private async Task InitializeAsync()
{
IsLoading = true;
try
{
var items = await _todoService.GetAllAsync();
Todos = new ObservableCollection<TodoItem>(items);
}
finally
{
IsLoading = false;
}
}
[RelayCommand(CanExecute = nameof(CanAddTodo))]
private async Task AddTodoAsync()
{
var newItem = new TodoItem
{
Title = NewTodoText,
CreatedAt = DateTime.UtcNow
};
await _todoService.CreateAsync(newItem);
Todos.Add(newItem);
NewTodoText = string.Empty;
}
private bool CanAddTodo()
=> !string.IsNullOrWhiteSpace(NewTodoText);
[RelayCommand]
private async Task ToggleCompleteAsync(TodoItem item)
{
item.IsCompleted = !item.IsCompleted;
await _todoService.UpdateAsync(item);
}
[RelayCommand]
private async Task DeleteTodoAsync(TodoItem item)
{
await _todoService.DeleteAsync(item.Id);
Todos.Remove(item);
}
partial void OnFilterTextChanged(string value)
{
// Reaktivní filtrování při změně textu
_ = FilterTodosAsync(value);
}
private async Task FilterTodosAsync(string filter)
{
var allItems = await _todoService.GetAllAsync();
var filtered = string.IsNullOrWhiteSpace(filter)
? allItems
: allItems.Where(t =>
t.Title.Contains(filter,
StringComparison.OrdinalIgnoreCase));
Todos = new ObservableCollection<TodoItem>(filtered);
}
}
Tenhle ViewModel demonstruje praktické použití všech klíčových prvků:
[ObservableProperty]pro reaktivní vlastnosti[RelayCommand]pro synchronní i asynchronní příkazyCanExecutepro řízení dostupnosti tlačítka přidání[NotifyCanExecuteChangedFor]pro automatickou aktualizaci stavu příkazu- Parciální metoda
OnFilterTextChangedpro reaktivní filtrování - Constructor injection pro závislosti
Odpojení XAML: Data binding v praxi
Propojení ViewModelu s XAML stránkou je v .NET MAUI přímočaré. Ukažme si XAML pro náš TodoViewModel:
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MauiApp.ViewModels"
x:Class="MauiApp.Views.TodoPage"
x:DataType="vm:TodoViewModel"
Title="Moje úkoly">
<Grid RowDefinitions="Auto,*" Padding="16">
<!-- Vstupní pole pro nový úkol -->
<Grid ColumnDefinitions="*,Auto">
<Entry
Text="{Binding NewTodoText}"
Placeholder="Nový úkol..."
ReturnCommand="{Binding AddTodoCommand}" />
<Button
Grid.Column="1"
Text="Přidat"
Command="{Binding AddTodoCommand}" />
</Grid>
<!-- Seznam úkolů -->
<CollectionView
Grid.Row="1"
ItemsSource="{Binding Todos}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="model:TodoItem">
<SwipeView>
<SwipeView.RightItems>
<SwipeItems>
<SwipeItem
Text="Smazat"
BackgroundColor="Red"
Command="{Binding
Source={RelativeSource
AncestorType=
{x:Type vm:TodoViewModel}},
Path=DeleteTodoCommand}"
CommandParameter=
"{Binding .}" />
</SwipeItems>
</SwipeView.RightItems>
<Grid
Padding="8"
ColumnDefinitions="Auto,*">
<CheckBox
IsChecked=
"{Binding IsCompleted}" />
<Label
Grid.Column="1"
Text="{Binding Title}"
VerticalOptions="Center" />
</Grid>
</SwipeView>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- Loading indikátor -->
<ActivityIndicator
Grid.RowSpan="2"
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"
HorizontalOptions="Center"
VerticalOptions="Center" />
</Grid>
</ContentPage>
Důležité: Všimněte si atributu x:DataType na stránce i v DataTemplate. Tím zapínáte kompilované bindingy, které jsou výrazně rychlejší než reflexní a navíc zachytí chyby v názvech vlastností už při kompilaci. Tohle je jedna z věcí, na kterou se často zapomíná — a přitom je to skoro zadarmo.
Testování ViewModelů
Jednou z největších výhod MVVM je testovatelnost. A tady se to celé vyplatí. ViewModely vytvořené s CommunityToolkit.Mvvm lze snadno testovat pomocí xUnit:
public class TodoViewModelTests
{
private readonly Mock<ITodoService> _mockService;
private readonly TodoViewModel _viewModel;
public TodoViewModelTests()
{
_mockService = new Mock<ITodoService>();
_viewModel = new TodoViewModel(_mockService.Object);
}
[Fact]
public void AddTodoCommand_IsDisabled_WhenTextIsEmpty()
{
// Arrange
_viewModel.NewTodoText = string.Empty;
// Act & Assert
Assert.False(_viewModel.AddTodoCommand.CanExecute(null));
}
[Fact]
public void AddTodoCommand_IsEnabled_WhenTextIsNotEmpty()
{
// Arrange
_viewModel.NewTodoText = "Nový úkol";
// Act & Assert
Assert.True(_viewModel.AddTodoCommand.CanExecute(null));
}
[Fact]
public async Task LoadData_SetsProducts()
{
// Arrange
var items = new List<TodoItem>
{
new() { Title = "Úkol 1" },
new() { Title = "Úkol 2" }
};
_mockService.Setup(s => s.GetAllAsync())
.ReturnsAsync(items);
// Act
await _viewModel.InitializeCommand.ExecuteAsync(null);
// Assert
Assert.Equal(2, _viewModel.Todos.Count);
Assert.False(_viewModel.IsLoading);
}
}
ViewModely jsou čisté C# třídy bez závislosti na .NET MAUI frameworku. Testy tak běží v běžném xUnit projektu bez emulátorů — v řádu milisekund. Když si zvyknete psát ViewModely tímhle způsobem, unit testy přestanou být něco, co „by se mělo dělat", a stanou se přirozenou součástí workflow.
Nejčastější chyby a jak se jim vyhnout
Za dobu práce s CommunityToolkit.Mvvm jsem narazil na několik problémů, které se opakují pořád dokola. Tady jsou ty nejčastější:
1. Chybějící klíčové slovo partial
Tohle je jednička. Source generátory fungují pouze s parciálními třídami. Vynecháte-li partial, kód se zkompiluje, ale vygenerované vlastnosti a příkazy nebudou existovat:
// ŠPATNĚ — nebude fungovat
public class MyViewModel : ObservableObject
{
[ObservableProperty]
private string _name; // Generátor toto ignoruje
}
// SPRÁVNĚ
public partial class MyViewModel : ObservableObject
{
[ObservableProperty]
private string _name; // Generátor vytvoří vlastnost Name
}
2. Přístup k privátnímu poli místo k vygenerované vlastnosti
Tahle chyba je zákeřnější. Pokud ve ViewModelu přistupujete přímo k privátnímu poli (_name) místo k vygenerované vlastnosti (Name), obejdete mechanismus notifikací a UI se neaktualizuje:
// ŠPATNĚ — UI se neaktualizuje
_name = "nová hodnota";
// SPRÁVNĚ — spustí PropertyChanged
Name = "nová hodnota";
Dobrá zpráva — toolkit 8.4 obsahuje diagnostický analyzátor, který vás na tohle upozorní varovnou zprávou během kompilace.
3. Zapomenutá registrace v DI kontejneru
Zaregistrujete stránku, ale zapomenete na ViewModel (nebo naopak)? Navigace selže s výjimkou. Vždy registrujte oboje:
builder.Services.AddTransient<MyPage>();
builder.Services.AddTransient<MyViewModel>(); // Nezapomeňte!
4. Použití ObservableCollection bez správného thread přístupu
Modifikace ObservableCollection z background threadu způsobí výjimku. Vždycky použijte MainThread.BeginInvokeOnMainThread nebo zajistěte, že asynchronní příkazy vrací výsledky na UI thread:
[RelayCommand]
private async Task LoadAsync()
{
var items = await _service.GetAllAsync();
// Bezpečná aktualizace na UI threadu
MainThread.BeginInvokeOnMainThread(() =>
{
Items.Clear();
foreach (var item in items)
Items.Add(item);
});
}
Často kladené otázky (FAQ)
Jaký je rozdíl mezi CommunityToolkit.Mvvm a Prism pro .NET MAUI?
CommunityToolkit.Mvvm je lehký, modulární toolkit zaměřený čistě na MVVM vzor — source generátory, příkazy a messaging. Prism je naproti tomu komplexní framework, který navíc řeší navigaci, modularitu a dialog služby. Pokud potřebujete jen MVVM infrastrukturu, CommunityToolkit je lepší volba díky menšímu otisku a jednoduchosti. Potřebujete-li plnohodnotný application framework s konvencemi pro navigaci a moduly, podívejte se na Prism.
Je CommunityToolkit.Mvvm kompatibilní s NativeAOT a trimmingem?
Ano, plně. Od verze 8.3 je celý balíček trim-safe a AOT-kompatibilní. Source generátory produkují kód, který nevyžaduje reflexi za běhu — a to je zásadní výhoda oproti starším MVVM frameworkům. S partial properties (v8.4+) je kompatibilita ještě lepší.
Mohu CommunityToolkit.Mvvm použít i v jiných projektech než .NET MAUI?
Rozhodně ano. Balíček je plně nezávislý na UI frameworku. Funguje s WPF, WinUI 3, Avalonia UI, Uno Platform, a klidně i s konzolovými aplikacemi. ViewModely vytvořené s tímto toolkitem jsou přenositelné mezi projekty bez jakýchkoli úprav.
Jak migrovat z Xamarin.Forms Command na RelayCommand?
Migrace je přímočará. Nahraďte Xamarin.Forms.Command za [RelayCommand] atribut na metodě. Odstraňte ruční vytváření příkazů v konstruktoru a zajistěte, že třída dědí z ObservableObject a je označena jako partial. Bindingy v XAML zůstávají stejné — vlastnost příkazu se jen přejmenuje podle konvence (název metody + Command).
Jak sdílet stav mezi více ViewModely?
Máte tři hlavní cesty: (1) použijte WeakReferenceMessenger pro volně vázanou komunikaci, (2) vytvořte sdílenou službu registrovanou jako Singleton v DI kontejneru, nebo (3) kombinujte oba přístupy. Pro jednorázové notifikace (přidán produkt, odhlášen uživatel) je messenger ideální. Pro průběžný sdílený stav (aktuální uživatel, nastavení aplikace) je lepší sdílená služba.