Here's a short example of Toast Messages using MVVM programming pattern.
Disclamer: I did it by myself from the scratch and I'm not a professional programmer. WPF is my hobby not work. Thus, the solution tested but may contain bugs or inaccurate implementations. Don't trust me.
Due to the pattern approach, we need two helper classes
// INPC interface implementation, used for notifying UI if some Property was changed. public class NotifyPropertyChanged : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } // Needed for easy Commands use public class RelayCommand : ICommand { private readonly Action<object> _execute; private readonly Func<object, bool> _canExecute; public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null) { _execute = execute; _canExecute = canExecute; } public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter); public void Execute(object parameter) => _execute(parameter); }
The solution features:
- Shows in the right-bottom corner Toast message
- Supports 3 severities: Info, Warning, Error. The color of message depends on it.
- Supports adjusting max count of messages for displaying at once
- Enqueues recieved above limit messages and show it later
- User can close any message immediately
- Each message dissapear after ~10 seconds
- Some appearance and disappearance animations added
Note:
- No support for concurrency e.g. pushing messages from different Threads/Tasks.
- Not a UserControl or standalone solution.
...but you may improve it. :)
The data class:
public enum ToastMessageSeverity { Info = 0, Warning = 1, Error = 2 } public class ToastMessage { public string Message { get; set; } public ToastMessageSeverity Severity { get; set; } }
ToastViewModel
public class ToastViewModel : ReadOnlyObservableCollection<ToastMessage> { private readonly ObservableCollection<ToastMessage> _items; private readonly int _maxCount; private readonly Queue<ToastMessage> _messagesQueue; private ICommand _removeItem; private void RemoveMessage(ToastMessage message) { if (_items.Contains(message)) { _items.Remove(message); if (_messagesQueue.Count > 0) Push(_messagesQueue.Dequeue()); } } public async void Push(ToastMessage message) { if (_items.Count >= _maxCount) _messagesQueue.Enqueue(message); else { _items.Add(message); await Task.Delay(10500); RemoveMessage(message); } } public ICommand RemoveItem => _removeItem ?? (_removeItem = new RelayCommand(parameter => { if (parameter is ToastMessage message) RemoveMessage(message); })); public ToastViewModel() : this(6) { } public ToastViewModel(int maxCount) : this(new ObservableCollection<ToastMessage>(), maxCount) { } private ToastViewModel(ObservableCollection<ToastMessage> items, int maxCount) : base(items) { _items = items; _maxCount = maxCount; _messagesQueue = new Queue<ToastMessage>(); } }
MainViewModel
public class MainViewModel : NotifyPropertyChanged { private ToastViewModel _toastMessages; private ICommand _pushToastCommand; public ToastViewModel ToastMessages { get => _toastMessages; set { _toastMessages = value; OnPropertyChanged(); } } private int counter = 0; public ICommand PushToastCommand => _pushToastCommand ?? (_pushToastCommand = new RelayCommand(parameter => { ToastMessageSeverity severity = ToastMessageSeverity.Info; if (parameter is string severityString) { foreach (ToastMessageSeverity tms in Enum.GetValues(typeof(ToastMessageSeverity))) { if (severityString == tms.ToString()) { severity = tms; break; } } } ToastMessages.Push(new ToastMessage { Message = severity + " message " + counter++, Severity = severity }); })); public MainViewModel() { ToastMessages = new ToastViewModel(); } }
And full markup (will allow to reproduce the entire app)
<Window x:Class="WpfApp1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApp1" Title="MainWindow" Height="600" Width="1000" WindowStartupLocation="CenterScreen"> <Window.DataContext> <local:MainViewModel/> </Window.DataContext> <Grid> <Grid> <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center"> <Button Content="Info message" Command="{Binding PushToastCommand}" Padding="10,5" Margin="10" /> <Button Content="Warning message" Command="{Binding PushToastCommand}" CommandParameter="Warning" Padding="10,5" Margin="10" /> <Button Content="Error message" Command="{Binding PushToastCommand}" CommandParameter="Error" Padding="10,5" Margin="10" /> </StackPanel> </Grid> <Grid> <ItemsControl ItemsSource="{Binding ToastMessages}" Margin="10" HorizontalAlignment="Right" VerticalAlignment="Bottom"> <ItemsControl.ItemTemplate> <DataTemplate> <Border HorizontalAlignment="Right" > <Border.Style> <Style TargetType="Border"> <Setter Property="BorderThickness" Value="2"/> <Setter Property="CornerRadius" Value="5"/> <Setter Property="Margin" Value="10,5"/> <Setter Property="Padding" Value="15,10"/> <Setter Property="MaxWidth" Value="300"/> <Style.Triggers> <DataTrigger Binding="{Binding Severity}" Value="Info"> <Setter Property="BorderBrush" Value="CadetBlue"/> <Setter Property="Background" Value="LightCyan"/> </DataTrigger> <DataTrigger Binding="{Binding Severity}" Value="Warning"> <Setter Property="BorderBrush" Value="Orange"/> <Setter Property="Background" Value="LightYellow"/> </DataTrigger> <DataTrigger Binding="{Binding Severity}" Value="Error"> <Setter Property="BorderBrush" Value="Red"/> <Setter Property="Background" Value="LightPink"/> </DataTrigger> <EventTrigger RoutedEvent="Border.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" Duration="0:0:0.5" /> <ThicknessAnimation Storyboard.TargetProperty="Margin" From="10,15,10,-5" To="10,5" Duration="0:0:0.5" /> <DoubleAnimation Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" BeginTime="0:0:10" Duration="0:0:0.2" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Style.Triggers> </Style> </Border.Style> <Grid> <TextBlock Text="{Binding Message}" FontSize="16" TextWrapping="Wrap" /> <Button HorizontalAlignment="Right" VerticalAlignment="Top" Margin="-13" Command="{Binding ItemsSource.RemoveItem, RelativeSource={RelativeSource AncestorType=ItemsControl}}" CommandParameter="{Binding}"> <Button.Template> <ControlTemplate> <TextBlock Text="×" FontSize="16" Foreground="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType=Border}}" Cursor="Hand"/> </ControlTemplate> </Button.Template> </Button> </Grid> </Border> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Grid> </Grid> </Window>

And traditionally for MVVM newcomers: code-behind class
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } }