The Atc.Wpf Source Generators 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.
Let's start by defining a ViewModel using source generators.
public partial class TestViewModel : ViewModelBase
{
[ObservableProperty]
private string name;
}
ObservablePropertyAttribute
automatically generates theName
property, includingINotifyPropertyChanged
support.RelayCommand
generates 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:
PropertyName
for customization.DependentProperties
for 1 to many other properties to be notified.DependentCommands
for 1 to many other commands to be notified.BeforeChangedCallback
is executed before the property value changes.AfterChangedCallback
is 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), DependentProperties = [nameof(FullName), nameof(Age)])]
private string name;
// Generates a property named "MyName" and notifies ApplyCommand and SaveCommand
[ObservableProperty(nameof(MyName), DependentCommands = [nameof(ApplyCommand) nameof(SaveCommand)])]
private string name;
// Generates a property named "Name" and notifies FullName and Age
[ObservableProperty(DependentProperties = [nameof(FullName), nameof(Age)])]
// Notifies the property "Email"
[NotifyPropertyChangedFor(nameof(Email))]
// Notifies multiple properties
[NotifyPropertyChangedFor(nameof(FullName), nameof(Age))]
Note:
NotifyPropertyChangedFor
ensures that when the annotated property changes, specified dependent properties also get notified.
// 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:
CommandName
for customization.CanExecute
a property or method that returnbool
to specified to control when the command is executable.ParameterValue
orParameterValues
for 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();
Note:
- The
RelayCommand
attribute generates anIRelayCommand
property linked to the annotated method. CanExecute
logic 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 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)
// Generates an RelayCommand with and CanExecute function and will be executed on a background thread
[RelayCommand(CanExecute = nameof(CanSave), ExecuteOnBackgroundThread = true)]
public void Save();
// Generates an asynchronous RelayCommand with async keyword and CanExecute function and will be executed on a background thread
[RelayCommand(CanExecute = nameof(CanSave), ExecuteOnBackgroundThread = true)]
public async Task Save();
// Generates an RelayCommand and toggles IsBusy around a synchronous command execution
[RelayCommand(CanExecute = nameof(CanSave), AutoSetIsBusy = true)]
public void Save();
// Generates an RelayCommand and toggles IsBusy around a synchronous, background‑thread command execution
[RelayCommand(CanExecute = nameof(CanSave), ExecuteOnBackgroundThread = true, AutoSetIsBusy = true)]
public void Save();
// Generates an asynchronous RelayCommand and toggles IsBusy around an asynchronous command execution
[RelayCommand(CanExecute = nameof(CanSave), AutoSetIsBusy = true)]
public async Task Save();
// Generates an asynchronous RelayCommand and toggles IsBusy around an asynchronous, background‑thread command execution
[RelayCommand(CanExecute = nameof(CanSave), ExecuteOnBackgroundThread = true, AutoSetIsBusy = true)]
public async Task Save();
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
ObservableProperty
to eliminate manual property creation. - ✔️ Use
NotifyPropertyChangedFor
to notify dependent properties. - ✔️ Use
RelayCommand
for automatic command generation. - ✔️ Use async commands for better UI responsiveness.
- ✔️ Improve performance by leveraging
CanExecute
for 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.
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]
[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}";
[RelayCommand]
public void ShowData()
{
// TODO: Implement ShowData - it could be a dialog box
}
[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()
{
// TODO: Implement validation
return true;
}
}
public partial class PersonViewModel
{
public IRelayCommand ShowDataCommand => new RelayCommand(ShowData);
public IRelayCommand SaveHandlerCommand => new RelayCommand(SaveHandler, CanSaveHandler);
public string FirstName
{
get => firstName;
set
{
if (firstName == value)
{
return;
}
firstName = value;
RaisePropertyChanged(nameof(FirstName));
RaisePropertyChanged(nameof(FullName));
}
}
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));
}
}
}