avalonia-mvvm
Guide for using MVVM Community Toolkit with Avalonia in RemoteViewer.Client. Use when working with ViewModels, data binding, commands, or Avalonia UI patterns.
安装与使用
请根据来源仓库和你的 AI 工具说明安装此技能。先阅读技能文件和依赖要求,再添加到工作流。
查看来源 ↗使用指南技能说明
Avalonia MVVM Patterns (RemoteViewer.Client)
The RemoteViewer.Client uses CommunityToolkit.Mvvm v8.4.0 with Avalonia's compiled bindings for a modern, low-boilerplate MVVM implementation.
Core Patterns
ViewModels
-
Base class:
ObservableObjectfrom CommunityToolkit.Mvvm -
Location: Co-located with views in
Views/{Feature}/directories -
Creation: Use
IViewModelFactorywith dependency injection for instantiation - Constructor injection: All dependencies injected via constructor (services, logger, etc.)
Main ViewModels:
-
MainViewModel: Login/connection window (src/RemoteViewer.Client/Views/Main/) -
ViewerViewModel: Remote desktop viewer session (src/RemoteViewer.Client/Views/Viewer/) -
PresenterViewModel: Remote desktop presenter session (src/RemoteViewer.Client/Views/Presenter/) -
ChatViewModel,ToastsViewModel: Nested component ViewModels
Observable Properties
Use [ObservableProperty] attribute for automatic property generation:
[ObservableProperty]
private string? _statusText = "Connecting...";
[ObservableProperty]
private bool _isConnected;
Pattern: Private backing field with underscore prefix → Source-generated public property with PropertyChanged notifications
Commands
Use [RelayCommand] attribute for automatic command generation:
// Synchronous command
[RelayCommand]
private void ToggleFullscreen()
{
this.IsFullscreen = !this.IsFullscreen;
}
// Async command
[RelayCommand]
private async Task ConnectToDeviceAsync()
{
var error = await this._hubClient.ConnectTo(username, password);
}
// Command with CanExecute
private bool CanSendMessage() => !string.IsNullOrWhiteSpace(this.MessageInput);
[RelayCommand(CanExecute = nameof(CanSendMessage))]
private async Task SendMessageAsync()
{
await this.Connection.SendMessageAsync(this.MessageInput);
}
Generated: XxxCommand property (IRelayCommand/IAsyncRelayCommand) with automatic CanExecuteChanged handling
Property Dependencies
Use [NotifyPropertyChangedFor] for computed properties:
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CharactersRemaining))]
[NotifyPropertyChangedFor(nameof(IsAtLimit))]
private string _messageInput = string.Empty;
public int CharactersRemaining => this.MaxLength - this.MessageInput.Length;
public bool IsAtLimit => this.MessageInput.Length >= this.MaxLength;
Command State Dependencies
Use [NotifyCanExecuteChangedFor] to invalidate commands when properties change:
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ToggleInputCommand))]
[NotifyCanExecuteChangedFor(nameof(SendCtrlAltDelCommand))]
private bool _isInputBlockedByPresenter;
private bool CanToggleInput() => !this.IsInputBlockedByPresenter;
[RelayCommand(CanExecute = nameof(CanToggleInput))]
private async Task ToggleInputAsync()
{
this.IsInputEnabled = !this.IsInputEnabled;
}
Property Change Handlers
Use partial methods for custom logic on property changes:
[ObservableProperty]
private bool _isOpen;
partial void OnIsOpenChanged(bool value)
{
if (value)
this.HasUnreadMessages = false;
}
View-ViewModel Binding
XAML Binding
Use x:DataType for compiled bindings (Avalonia 11.3 feature):
<Window x:DataType="vm:MainViewModel">
<!-- Property binding -->
<TextBlock Text="{Binding StatusText}"/>
<!-- Command binding -->
<Button Command="{Binding ConnectCommand}"/>
</Window>
Code-Behind Pattern
Type-safe ViewModel access with event management:
public partial class MainView : Window
{
private MainViewModel? _viewModel;
private void Window_DataContextChanged(object? sender, EventArgs e)
{
// Unsubscribe from old ViewModel
if (this._viewModel is not null)
this._viewModel.RequestShowMainView -= this.ViewModel_RequestShowMainView;
this._viewModel = this.DataContext as MainViewModel;
// Subscribe to new ViewModel
if (this._viewModel is not null)
this._viewModel.RequestShowMainView += this.ViewModel_RequestShowMainView;
}
}
Pattern: Always unsubscribe from old ViewModel events to prevent memory leaks
Dependency Injection
Service Registration
Services registered in ServiceRegistration.cs:
services.AddSingleton<IViewModelFactory, ViewModelFactory>();
services.AddSingleton<IClipboardService, AvaloniaClipboardService>();
services.AddSingleton<IDispatcher, AvaloniaDispatcher>();
services.AddSingleton<ConnectionHubClient>();
ViewModel Factory Pattern
Use IViewModelFactory for creating ViewModels with DI:
public interface IViewModelFactory
{
MainViewModel CreateMainViewModel();
ViewerViewModel CreateViewerViewModel(Connection connection);
}
public class ViewModelFactory(IServiceProvider serviceProvider) : IViewModelFactory
{
public MainViewModel CreateMainViewModel() =>
ActivatorUtilities.CreateInstance<MainViewModel>(serviceProvider);
public ViewerViewModel CreateViewerViewModel(Connection connection) =>
ActivatorUtilities.CreateInstance<ViewerViewModel>(serviceProvider, connection);
}
Usage: Factory methods support both DI parameters and runtime parameters (e.g., Connection)
Thread Safety
Use IDispatcher service for thread-safe UI updates:
private readonly IDispatcher _dispatcher;
// From background thread
this._dispatcher.Post(() =>
{
this.StatusText = "Connected";
this.IsConnected = true;
});
Pattern: Always use dispatcher when updating UI properties from non-UI threads
ViewModel Communication
Use standard .NET events for ViewModel-to-View communication:
// In ViewModel
public event EventHandler? CloseRequested;
public event EventHandler? RequestShowMainView;
private void RaiseCloseRequest()
{
this.CloseRequested?.Invoke(this, EventArgs.Empty);
}
// In View code-behind
private void ViewModel_CloseRequested(object? sender, EventArgs e)
{
this.Close();
}
Resource Cleanup
ViewModels implement IAsyncDisposable for proper cleanup:
public async ValueTask DisposeAsync()
{
if (this._disposed)
return;
this._disposed = true;
// Cleanup resources
await this.Connection.DisconnectAsync();
// Unsubscribe from events
this.Connection.ParticipantsChanged -= this.Connection_ParticipantsChanged;
GC.SuppressFinalize(this);
}
Important: Always unsubscribe from events in Dispose to prevent memory leaks
Key Benefits
- Minimal boilerplate: Source generators eliminate repetitive code
- Type-safe: Compiled bindings catch errors at build time
- Testable: Full dependency injection support
- Performant: Avalonia compiled bindings with zero reflection
- Maintainable: Consistent patterns across all ViewModels
使用示例
查看来源仓库中的使用示例与配置说明。
查看来源 ↗来源历史
- 收录时间
- 2026-01-20
- 索引更新时间
- 2026-01-26
- Branch
- main
收录与同步日期属于本站记录。源码修改历史请查看来源仓库。
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!