Search code examples
c#wpfclassnotificationstoast

Trying to create a Class Toast Notification, cannot instance it multiple times


I am trying to create a Toast Notification in WPF, my problem is that if I, in this case, delete something from the database too quickly, the current notification just gets upgraded with the new text instead of creating a new instance of the class, so I can see two notifications.

The notification becomes a child of a StackPanelfrom the MainWindow, that way it always has the same position. That is why I clear the children at the beginning.

The class works, it's just that I cannot manage to have more than one notification at a time.

What am I missing? Sorry, I am quite a beginner and I tried to do this by myself.

This is my class code

{
   public class NotificationToast
    {
        public Border ToastBorder { get; set; }
        public Grid ToastGrid { get; set; }
        public TextBlock BodyBlock { get; set; }
        public String NotificationString { get; set; }
        public DoubleAnimation Animation1 { get; set; }
        public DoubleAnimation Animation2 { get; set; }
        public TranslateTransform Transformation1 { get; set; }
        public TranslateTransform Transformation2 { get; set; }

        public NotificationToast(MainWindow window, string notificationString)
        {
            InitializeWindow(window, notificationString);   
        }

        private void InitializeWindow(MainWindow window, string notificationString)
        {
            NotificationString = notificationString;
            ToastBorder = new Border();
            ToastBorder.Width = 250;
            ToastBorder.Height = 70;
            ToastBorder.BorderThickness = new Thickness(2);
            ToastBorder.BorderBrush = Brushes.IndianRed;
            ToastBorder.Background = new SolidColorBrush(Color.FromRgb(240, 143, 116));
            TextBlock BodyBlock = new TextBlock();
            BodyBlock.Width = 248;
            BodyBlock.Height = 68;
            BodyBlock.TextWrapping = TextWrapping.Wrap;
            BodyBlock.FontSize = 16;
            BodyBlock.Text = NotificationString;
            BodyBlock.Margin = new Thickness(5);

            ToastBorder.Child = BodyBlock;
            window.stackNotification.Children.Clear();
            window.stackNotification.Children.Add(ToastBorder);

            MovingIn(window.stackNotification);
            MovingOut(window.stackNotification);

        }

        private void MovingIn(StackPanel movingBorder)
        {
            TranslateTransform trans = new TranslateTransform();
            movingBorder.RenderTransform = trans;
            Animation1 = new DoubleAnimation(80, 0, TimeSpan.FromSeconds(1));
            trans.BeginAnimation(TranslateTransform.YProperty, Animation1);
        }
        private async void MovingOut(StackPanel movingBorder)
        {
            await Task.Delay(2500);

            TranslateTransform trans = new TranslateTransform();
            movingBorder.RenderTransform = trans;
            Animation2 = new DoubleAnimation(0, 80, TimeSpan.FromSeconds(1));
            trans.BeginAnimation(TranslateTransform.YProperty, Animation2);
        }
    }
}

And then I call the class like this

WindowToast = new NotificationToast(ParentWindow, Convert.ToString("The Player " + PersonDetails.FirstName + " " + PersonDetails.LastName + " details has been updated."));

Solution

  • 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>
    

    Toast messages

    And traditionally for MVVM newcomers: code-behind class

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }