The Atc.XamlToolkit Source Generators (included as a dependency of Atc.Wpf) simplify ViewModel development by reducing boilerplate code for properties and commands. With attributes like ObservableProperty and RelayCommand, you can focus on business logic while automatically handling property change notifications and command implementations.
Note: The source generators are provided by the
Atc.XamlToolkitandAtc.XamlToolkit.WpfNuGet packages, which are automatically included when you referenceAtc.Wpf.
Let's start by defining a ViewModel using source generators.
public partial class TestViewModel : ViewModelBase
{
[ObservableProperty]
private string name;
}ObservablePropertyAttributeautomatically generates theNameproperty, includingINotifyPropertyChangedsupport.RelayCommandgenerates aSayHelloCommand, which can be bound to a button in the UI.
<UserControl xmlns:local="clr-namespace:MyApp.MyUserControl">
<UserControl.DataContext>
<local:TestViewModel/>
</UserControl.DataContext>
<StackPanel>
<TextBox Text="{Binding Path=Name, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="Say Hello" Command="{Binding Path=SayHelloCommand}" />
</StackPanel>
</UserControl>This setup allows the UI to dynamically update when the Name property changes.
The ObservableProperty attribute automatically generates properties from private fields, including INotifyPropertyChanged support.
ObservableProperty options:
PropertyNamefor customization.DependentPropertiesfor 1 to many other properties to be notified.BeforeChangedCallbackis executed before the property value changes.AfterChangedCallbackis executed after the property value changes.
// Generates a property named "Name"
[ObservableProperty()]
private string name;
// Generates a property named "MyName"
[ObservableProperty("MyName")]
private string name;
// Generates a property named "MyName" and notifies FullName and Age
[ObservableProperty(nameof(MyName), nameof(FullName), nameof(Age))]
private string name;
// Generates a property named "MyName" and notifies FullName and Age
[ObservableProperty(nameof(MyName), DependentProperties = [nameof(FullName), nameof(Age)])]
private string name;
// Generates a property named "Name" and broadcast message
[ObservableProperty(BroadcastOnChange = true)]
private string name;// Notifies multiple propertries as FullName and Age
[ObservableProperty(DependentProperties = [nameof(FullName), nameof(Age)])]
// Notifies the property "Email"
[NotifyPropertyChangedFor(nameof(Email))]
// Notifies multiple propertries as FullName and Age
[NotifyPropertyChangedFor(nameof(FullName), nameof(Age))]Note:
NotifyPropertyChangedForensures that when the annotated property changes, specified dependent properties also get notified.
// Notifies by broadcast a message by type PropertyChangedMessage
[ObservableProperty(BroadcastOnChange = true)]// Calls DoStuff before the property changes
[ObservableProperty(BeforeChangedCallback = nameof(DoStuff))]
// Calls DoStuff after the property changes
[ObservableProperty(AfterChangedCallback = nameof(DoStuff))]
// Calls DoStuffA before and DoStuffB after the property changes
[ObservableProperty(
BeforeChangedCallback = nameof(DoStuffA),
AfterChangedCallback = nameof(DoStuffB))]
// Executes inline code before and after the property changes
// - Executes DoStuffA before the change
// - Executes event and DoStuffB after the change
[ObservableProperty(
BeforeChangedCallback = "DoStuffA();",
AfterChangedCallback = "EntrySelected?.Invoke(this, selectedEntry); DoStuffB();")]The RelayCommand attribute generates IRelayCommand properties, eliminating manual command setup.
RelayCommand options:
CommandNamefor customization.CanExecutea property or method that returnboolto specified to control when the command is executable.InvertCanExecuteif the propertyCanExecuteis defined, this property will invert the outcome from the property or methodCanExecuterelays on.ParameterValueorParameterValuesfor 1 or many parameter values.
// Generates a RelayCommand named "SaveCommand"
[RelayCommand()]
public void Save();
// Generates a RelayCommand named "MySaveCommand"
[RelayCommand("MySave")]
public void Save();// Generates a RelayCommand that takes a string parameter
[RelayCommand()]
public void Save(string text);
// Generates a RelayCommand with CanExecute function
[RelayCommand(CanExecute = nameof(CanSave))]
public void Save();
// Generates a RelayCommand with CanExecute function
[RelayCommand(CanExecute = nameof(IsConnected), InvertCanExecute = true)]
public void Connect();Note:
- The
RelayCommandattribute generates anIRelayCommandproperty linked to the annotated method. CanExecutelogic can be specified to control when the command is executable.
// Generates an asynchronous RelayCommand
[RelayCommand()]
public Task Save();
// Generates an asynchronous RelayCommand with async keyword
[RelayCommand()]
public async Task Save();
// Generates an asynchronous RelayCommand named "MySaveCommand"
[RelayCommand("MySave")]
public Task Save();
// Generates an asynchronous RelayCommand named "MySaveCommand" with async keyword
[RelayCommand("MySave")]
public async Task Save();
// Generates an asynchronous RelayCommand that takes a string parameter
[RelayCommand()]
public Task Save(string text);
// Generates an asynchronous RelayCommand with async keyword and string parameter
[RelayCommand()]
public async Task Save(string text);
// Generates an asynchronous RelayCommand with CanExecute function
[RelayCommand(CanExecute = nameof(CanSave))]
public Task Save();
// Generates an asynchronous RelayCommand with async keyword and CanExecute function
[RelayCommand(CanExecute = nameof(CanSave))]
public async Task Save();
// Generates an asynchronous RelayCommand async keyword and CanExecute function
[RelayCommand(CanExecute = nameof(IsConnected), InvertCanExecute = true)]
public async Task Connect();// Generates multi asynchronous RelayCommand with async keyword with multiple parameters
[RelayCommand("MyTestLeft", ParameterValues = [LeftTopRightBottomType.Left, 1])]
[RelayCommand("MyTestTop", ParameterValues = [LeftTopRightBottomType.Top, 1])]
[RelayCommand("MyTestRight", ParameterValues = [LeftTopRightBottomType.Right, 1])]
[RelayCommand("MyTestBottom", ParameterValues = [LeftTopRightBottomType.Bottom, 1])]
public Task TestDirection(LeftTopRightBottomType leftTopRightBottomType, int steps)
// Generates multi asynchronous RelayCommand with async keyword and CanExecute function with multiple parameters
[RelayCommand("MyTestLeft", CanExecute = nameof(CanTestDirection), ParameterValues = [LeftTopRightBottomType.Left, 1])]
[RelayCommand("MyTestTop", CanExecute = nameof(CanTestDirection), ParameterValues = [LeftTopRightBottomType.Top, 1])]
[RelayCommand("MyTestRight", CanExecute = nameof(CanTestDirection), ParameterValues = [LeftTopRightBottomType.Right, 1])]
[RelayCommand("MyTestBottom", CanExecute = nameof(CanTestDirection), ParameterValues = [LeftTopRightBottomType.Bottom, 1])]
public Task TestDirection(LeftTopRightBottomType leftTopRightBottomType, int steps)public partial class UserProfileViewModel : ViewModelBase
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string firstName;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string lastName;
public string FullName => $"{FirstName} {LastName}";
[RelayCommand]
private void SaveProfile()
{
MessageBox.Show($"Profile Saved: {FullName}");
}
}<TextBox Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding FullName}" />
<Button Content="Save" Command="{Binding SaveProfileCommand}" />The FullName property updates automatically when FirstName or LastName changes
A ViewModel that fetches data asynchronously and enables/disables a button based on loading state.
public partial class DataViewModel : ViewModelBase
{
[ObservableProperty]
private string? data;
[ObservableProperty]
private bool isLoading;
[RelayCommand(CanExecute = nameof(CanFetchData))]
private async Task FetchData(CancellationToken cancellationToken)
{
IsLoading = true;
await Task.Delay(2000, cancellationToken).ConfigureAwait(false); // Simulate API call
Data = "Fetched Data from API";
IsLoading = false;
}
private bool CanFetchData() => !IsLoading;
}<Button Command="{Binding Path=FetchDataCommand}" Content="Fetch Data" />
<TextBlock Text="{Binding Path=Data}" />The button is disabled while data is being fetched, preventing multiple API calls.
✅ Ensure your ViewModel inherits from ViewModelBase, which includes INotifyPropertyChanged.
public partial class MyViewModel : ViewModelBase { }✅ Check if your command has a valid CanExecute method.
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save() { /* ... */ }
private bool CanSave() => !string.IsNullOrEmpty(Name);- ✔️ Use
ObservablePropertyto eliminate manual property creation. - ✔️ Use
NotifyPropertyChangedForto notify dependent properties. - ✔️ Use
RelayCommandfor automatic command generation. - ✔️ Use async commands for better UI responsiveness.
- ✔️ Improve performance by leveraging
CanExecutefor commands.
- ✅ Reduces boilerplate – Write less code, get more done.
- ✅ Improves maintainability – Focus on business logic instead of plumbing.
- ✅ Enhances MVVM architecture – Ensures best practices in WPF development.
- ✅ Cross-platform ready – Same patterns work in WPF, WinUI, and Avalonia via Atc.XamlToolkit.
public partial class TestViewModel : ViewModelBase
{
[ObservableProperty]
private string name;
}public partial class TestViewModel
{
public string Name
{
get => name;
set
{
if (name == value)
{
return;
}
name = value;
RaisePropertyChanged(nameof(Name));
}
}
}public partial class PersonViewModel : ViewModelBase
{
[ObservableProperty(BroadcastOnChange = true)]
[NotifyPropertyChangedFor(nameof(FullName))]
[Required]
[MinLength(2)]
private string firstName = "John";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName), nameof(Age))]
[NotifyPropertyChangedFor(nameof(Email))]
[NotifyPropertyChangedFor(nameof(TheProperty))]
private string? lastName = "Doe";
[ObservableProperty]
private int age = 27;
[ObservableProperty]
private string? email;
[ObservableProperty(nameof(TheProperty), nameof(FullName), nameof(Age))]
private string? myTestProperty;
public string FullName => $"{FirstName} {LastName}";
public bool IsConnected { get; set; }
[RelayCommand(
CanExecute = nameof(IsConnected),
InvertCanExecute = true)]
public void ShowData()
{
var dialogBox = new InfoDialogBox(
Application.Current.MainWindow!,
new DialogBoxSettings(DialogBoxType.Ok),
$"{FullName} (age {Age}) — {Email}");
dialogBox.Show();
}
[RelayCommand(CanExecute = nameof(CanSaveHandler))]
public void SaveHandler()
{
var dialogBox = new InfoDialogBox(
Application.Current.MainWindow!,
new DialogBoxSettings(DialogBoxType.Ok),
"Hello to SaveHandler method");
dialogBox.Show();
}
public bool CanSaveHandler()
=> !string.IsNullOrWhiteSpace(FirstName)
&& !string.IsNullOrWhiteSpace(LastName)
&& Age > 0;
}public partial class PersonViewModel
{
public IRelayCommand ShowDataCommand => new RelayCommand(ShowData, () => !IsConnected);
public IRelayCommand SaveHandlerCommand => new RelayCommand(SaveHandler, CanSaveHandler);
public string FirstName
{
get => firstName;
set
{
if (firstName == value)
{
return;
}
var oldValue = firstName;
firstName = value;
RaisePropertyChanged(nameof(FirstName));
RaisePropertyChanged(nameof(FullName));
Broadcast(nameof(FirstName), oldValue, value);
}
}
public string? LastName
{
get => lastName;
set
{
if (lastName == value)
{
return;
}
lastName = value;
RaisePropertyChanged(nameof(LastName));
RaisePropertyChanged(nameof(FullName));
RaisePropertyChanged(nameof(Age));
RaisePropertyChanged(nameof(Email));
RaisePropertyChanged(nameof(TheProperty));
}
}
public int Age
{
get => age;
set
{
if (age == value)
{
return;
}
age = value;
RaisePropertyChanged(nameof(Age));
}
}
public string? Email
{
get => email;
set
{
if (email == value)
{
return;
}
email = value;
RaisePropertyChanged(nameof(Email));
}
}
public string? TheProperty
{
get => myTestProperty;
set
{
if (myTestProperty == value)
{
return;
}
myTestProperty = value;
RaisePropertyChanged(nameof(TheProperty));
RaisePropertyChanged(nameof(FullName));
RaisePropertyChanged(nameof(Age));
}
}
}