One essential facet of a great application is keeping the end users up to date with what's going on in the application. If they click on a function button, they should be informed as to the progress or the status of the operation. Without adequate feedback, the user can be left wondering whether a particular operation worked and may attempt to run it several times, possibly causing errors.
It is, therefore, essential to implement a feedback system in our application framework. So far in this book, we've seen the name of the FeedbackManager class in a few places, although we've seen very little implementation. Let's now see how we can implement a working feedback system in our application framework, starting with the Feedback class that holds the individual feedback messages:
using System; using System.ComponentModel; using CompanyName.ApplicationName.DataModels.Enums; using CompanyName.ApplicationName.DataModels.Interfaces; using CompanyName.ApplicationName.Extensions; namespace CompanyName.ApplicationName.DataModels { public class Feedback : IAnimatable, INotifyPropertyChanged { private string message = string.Empty; private FeedbackType type = FeedbackType.None; private TimeSpan duration = new TimeSpan(0, 0, 4); private bool isPermanent = false; private Animatable animatable; public Feedback(string message, FeedbackType type, TimeSpan duration) { Message = message; Type = type; Duration = duration == TimeSpan.Zero ? this.duration : duration; IsPermanent = false; Animatable = new Animatable(this); } public Feedback(string message, bool isSuccess, bool isPermanent) : this(message, isSuccess ? FeedbackType.Success : FeedbackType.Error, TimeSpan.Zero) { IsPermanent = isPermanent; } public Feedback(string message, FeedbackType type) : this(message, type, TimeSpan.Zero) { } public Feedback(string message, bool isSuccess) : this(message, isSuccess ? FeedbackType.Success : FeedbackType.Error, TimeSpan.Zero) { } public Feedback() : this(string.Empty, FeedbackType.None) { } public string Message { get { return message; } set { message = value; NotifyPropertyChanged(); } } public TimeSpan Duration { get { return duration; } set { duration = value; NotifyPropertyChanged(); } } public FeedbackType Type { get { return type; } set { type = value; NotifyPropertyChanged(); } } public bool IsPermanent { get { return isPermanent; } set { isPermanent = value; NotifyPropertyChanged(); } } #region IAnimatable Members public Animatable Animatable { get { return animatable; } set { animatable = value; } } #endregion #region INotifyPropertyChanged Members ... #endregion } }
Note that our Feedback class implements the IAnimatable interface, which we saw earlier, along with the INotifyPropertyChanged interface. After declaring the private fields, we declare a number of useful constructor overloads.
In this example, we have hardcoded a default feedback display duration of four seconds for the duration field. In the main constructor, we set the Duration property dependent upon the value of the duration input parameter; if the input parameter is the TimeSpan.Zero field, then the default value is used, but if the input parameter is a non-zero value, it will be used.
The Message property will hold the feedback message; the Duration property specifies the length of time that the message will be displayed; the Type property uses the FeedbackType enumeration that we saw earlier to specify the type of the message, and the IsPermanent property dictates whether the message should be permanently displayed until the user manually closes it or not.
The implementation of our IAnimatable class is shown beneath the other properties, and simply consists of the Animatable property, but our implementation of the INotifyPropertyChanged interface has been omitted for brevity, as we are using the default implementation that we saw earlier.
Let's now see the FeedbackCollection class that will contain the individual Feedback instances:
using System.Collections.Generic; using System.Linq; namespace CompanyName.ApplicationName.DataModels.Collections { public class FeedbackCollection : BaseAnimatableCollection<Feedback> { public FeedbackCollection(IEnumerable<Feedback> feedbackCollection) : base(feedbackCollection) { } public FeedbackCollection() : base() { } public new void Add(Feedback feedback) { if (!string.IsNullOrEmpty(feedback.Message) && (Count == 0 || !this.Any(f => f.Message == feedback.Message))) base.Add(feedback); } public void Add(string message, bool isSuccess) { Add(new Feedback(message, isSuccess)); } } }
The FeedbackCollection class extends the BaseAnimatableCollection class, which we saw earlier, and sets its generic type parameter to the Feedback class. This is a very simple class and declares a couple of constructors, passing any input parameters straight through to the base class constructors.
In addition to this, it declares two Add methods, with the second simply creating a Feedback object from its input parameters and passing it to the first method. The first method first checks that the feedback message is not null or empty and that an identical message is not already contained in the feedback collection, before adding the new message to the collection.
Note that our current implementation uses the base class Add method to add the new items to the end of the feedback collection. We could alternatively use the Insert method from the base class here to add new items to the start of the collection instead.
Let's now look at the FeedbackManager class that uses these two classes internally:
using System.ComponentModel; using System.Runtime.CompilerServices; using CompanyName.ApplicationName.DataModels; using CompanyName.ApplicationName.DataModels.Collections; namespace CompanyName.ApplicationName.Managers { public class FeedbackManager : INotifyPropertyChanged { private static FeedbackCollection feedback = new FeedbackCollection(); private static FeedbackManager instance = null; private FeedbackManager() { } public static FeedbackManager Instance => instance ?? (instance = new FeedbackManager()); public FeedbackCollection Feedback { get { return feedback; } set { feedback = value; NotifyPropertyChanged(); } } public void Add(Feedback feedback) { Feedback.Add(feedback); } public void Add(string message, bool isSuccess) { Add(new Feedback(message, isSuccess)); } #region INotifyPropertyChanged Members ... #endregion } }
The FeedbackManager class also implements the INotifyPropertyChanged interface, and in it we see the static FeedbackCollection field. Next, we see the static instance field, the private constructor, and the static Instance property of type FeedbackManager, which instantiates the instance field on the first use and tells us that this class follows the Singleton pattern.
The Feedback property follows and is the class access to the FeedbackCollection field. After that, we see a number of convenient overloads of the Add method that enables developers to add feedback using different parameters. Our implementation of the INotifyPropertyChanged interface here has again been omitted for brevity, but it uses our default implementation that we saw earlier.
Let's now focus on the XAML of the FeedbackControl object:
<UserControl x:Class="CompanyName.ApplicationName.Views.Controls.FeedbackControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Controls="clr-namespace:CompanyName.ApplicationName.Views.Controls" xmlns:Converters="clr-namespace:CompanyName.ApplicationName.Converters; assembly=CompanyName.ApplicationName.Converters" xmlns:DataModels="clr-namespace:CompanyName.ApplicationName.DataModels; assembly=CompanyName.ApplicationName.DataModels" xmlns:Panels="clr-namespace:CompanyName.ApplicationName.Views.Panels"> <UserControl.Resources> <Converters:FeedbackTypeToImageSourceConverter x:Key="FeedbackTypeToImageSourceConverter" /> <Converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" /> <ItemsPanelTemplate x:Key="AnimatedPanel"> <Panels:AnimatedStackPanel /> </ItemsPanelTemplate> <Style x:Key="SmallImageInButtonStyle" TargetType="{x:Type Image}" BasedOn="{StaticResource ImageInButtonStyle}"> <Setter Property="Width" Value="16" /> <Setter Property="Height" Value="16" /> </Style> <DataTemplate x:Key="FeedbackTemplate" DataType="{x:Type DataModels:Feedback}"> <Grid Margin="2,1,2,0" MouseEnter="Border_MouseEnter" MouseLeave="Border_MouseLeave"> <Grid.ColumnDefinitions> <ColumnDefinition Width="16" /> <ColumnDefinition /> <ColumnDefinition Width="24" /> </Grid.ColumnDefinitions> <Image Stretch="None" Source="{Binding Type, Converter={StaticResource FeedbackTypeToImageSourceConverter}}" VerticalAlignment="Top" Margin="0,4,0,0" /> <TextBlock Grid.Column="1" Text="{Binding Message}" MinHeight="22" TextWrapping="Wrap" Margin="5,2,5,0" VerticalAlignment="Top" FontSize="14" /> <Button Grid.Column="2" ToolTip="Removes this message from the list" VerticalAlignment="Top" PreviewMouseLeftButtonDown= "DeleteButton_PreviewMouseLeftButtonDown"> <Image Source="pack://application:,,,/ CompanyName.ApplicationName;component/Images/Delete_16.png" Style="{StaticResource SmallImageInButtonStyle}" /> </Button> </Grid> </DataTemplate> <DropShadowEffect x:Key="Shadow" Color="Black" ShadowDepth="6" Direction="270" Opacity="0.4" /> </UserControl.Resources> <Border BorderBrush="{StaticResource TransparentBlack}" Background="White" Padding="3" BorderThickness="1,0,1,1" CornerRadius="0,0,5,5" Visibility="{Binding HasFeedback, Converter={StaticResource BoolToVisibilityConverter}, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Controls:FeedbackControl}}}" Effect="{StaticResource Shadow}"> <ListBox MaxHeight="89" ItemsSource="{Binding Feedback, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Controls:FeedbackControl}}}" ItemTemplate="{StaticResource FeedbackTemplate}" ItemsPanel="{StaticResource AnimatedPanel}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Auto" BorderThickness="0" HorizontalContentAlignment="Stretch" /> </Border> </UserControl>
We start by adding a number of XAML namespace prefixes for some of our application projects. Using the Converters prefix, we add instances of the FeedbackTypeToImageSourceConverter and BoolToVisibilityConverter classes that we saw earlier into the UserControl.Resources section. We also reuse our AnimatedStackPanel class from Chapter 7, Mastering Practical Animations.
Next, we see the SmallImageInButtonStyle style, which is based on the ImageInButtonStyle style that we also saw earlier, and adds some sizing properties. After that, we see the FeedbackStyle style that defines what each feedback message will look like in our feedback control.
Each Feedback object will be rendered in three columns: the first contains an image that specifies the type of feedback, using the FeedbackTypeToImageSourceConverter class that we saw earlier; the second displays the message with a TextWrapping value of Wrap; the third holds a button with an image, using our SmallImageInButtonStyle style, which users can use to remove the message.
Note that, as this is purely a UI control with no business logic in, we are able to use the code behind the file, even when using MVVM. As such, we attach event handlers for the MouseEnter and MouseLeave events to the Grid panel containing each Feedback object, and another for the PreviewMouseLeftButtonDown event to the delete button. The final resource that we have here is a DropShadowEffect instance that defines a small shadow effect.
For the feedback control, we define a Border element that uses a semi-transparent border brush and has a BorderThickness value of 1,0,1,1 and a CornerRadius value of 0,0,5,5. These four values work like the Margin property and enable us to set different values for each of the four sides, or corners in the case of the CornerRadius property. In this way, we can display a rectangle that is only bordered on three sides, with rounded corners on two.
Note that the Visibility property on this border is determined by the HasFeedback property of the FeedbackControl class via an instance of our BoolToVisibilityConverter class. Therefore, when there are no feedback objects to display, the border will be hidden. Also note that our Shadow resource is applied to the border Effect property.
Inside the border, we declare a ListBox control, with its ItemsSource property set to the Feedback property of the FeedbackControl class and its height restricted to a maximum of three feedback items, after which vertical scrollbars will be shown. Its ItemTemplate property is set to the FeedbackTemplate that we defined in the resources section.
Its ItemsPanel property is set to the AnimatedPanel resource that we declared to animate the entrance and exit of the feedback items. Next, we remove the default border of the ListBox by setting the BorderThickness property to 0 and stretch the autogenerated ListBoxItem objects to fit the width of the ListBox control by setting the HorizontalContentAlignment property to Stretch.
Let's now see the code behind our feedback control:
using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Threading; using CompanyName.ApplicationName.DataModels; using CompanyName.ApplicationName.DataModels.Collections; using CompanyName.ApplicationName.Extensions; namespace CompanyName.ApplicationName.Views.Controls { public partial class FeedbackControl : UserControl { private static List<DispatcherTimer> timers = new List<DispatcherTimer>(); public FeedbackControl() { InitializeComponent(); } public static readonly DependencyProperty FeedbackProperty = DependencyProperty.Register(nameof(Feedback), typeof(FeedbackCollection), typeof(FeedbackControl), new UIPropertyMetadata(new FeedbackCollection(), (d, e) => ((FeedbackCollection)e.NewValue).CollectionChanged += ((FeedbackControl)d).Feedback_CollectionChanged)); public FeedbackCollection Feedback { get { return (FeedbackCollection)GetValue(FeedbackProperty); } set { SetValue(FeedbackProperty, value); } } public static readonly DependencyProperty HasFeedbackProperty = DependencyProperty.Register(nameof(HasFeedback), typeof(bool), typeof(FeedbackControl), new PropertyMetadata(true)); public bool HasFeedback { get { return (bool)GetValue(HasFeedbackProperty); } set { SetValue(HasFeedbackProperty, value); } } private void Feedback_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if ((e.OldItems == null || e.OldItems.Count == 0) && e.NewItems != null && e.NewItems.Count > 0) { e.NewItems.OfType<Feedback>().Where(f => !f.IsPermanent). ForEach(f => InitializeTimer(f)); } HasFeedback = Feedback.Any(); } private void InitializeTimer(Feedback feedback) { DispatcherTimer timer = new DispatcherTimer(); timer.Interval = feedback.Duration; timer.Tick += Timer_Tick; timer.Tag = new Tuple<Feedback, DateTime>(feedback, DateTime.Now); timer.Start(); timers.Add(timer); } private void Timer_Tick(object sender, EventArgs e) { DispatcherTimer timer = (DispatcherTimer)sender; timer.Stop(); timer.Tick -= Timer_Tick; timers.Remove(timer); Feedback feedback = ((Tuple<Feedback, DateTime>)timer.Tag).Item1; Feedback.Remove(feedback); } private void DeleteButton_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { Button deleteButton = (Button)sender; Feedback feedback = (Feedback)deleteButton.DataContext; Feedback.Remove(feedback); } private void Border_MouseEnter(object sender, MouseEventArgs e) { foreach (DispatcherTimer timer in timers) { timer.Stop(); Tuple<Feedback, DateTime> tag = (Tuple<Feedback, DateTime>)timer.Tag; tag.Item1.Duration = timer.Interval = tag.Item1.Duration. Subtract(DateTime.Now.Subtract(tag.Item2)); } } private void Border_MouseLeave(object sender, MouseEventArgs e) { foreach (DispatcherTimer timer in timers) { Feedback feedback = ((Tuple<Feedback, DateTime>)timer.Tag).Item1; timer.Tag = new Tuple<Feedback, DateTime>(feedback, DateTime.Now); timer.Start(); } } } }
We start by declaring the collection of DispatcherTimer instances that will be responsible for timing when each feedback object should be removed from the collection, according to its Duration property. We then see the declaration of the Feedback and HasFeedback Dependency Properties, along with their CLR wrappers and the Feedback property's CollectionChanged handler.
In the attached Feedback_CollectionChanged handler method, we call the InitializeTimer method, passing in each new non-permanent feedback item. Note that we need to use the OfType LINQ Extension Method to cast each item in the NewItems property of the NotifyCollectionChangedEventArgs class from type object to Feedback. Before returning control to the caller, we set the HasFeedback property accordingly.
In the InitializeTimer method, we initialize a DispatcherTimer instance and set its interval to the value from the Duration property of the feedback input parameter. We then attach the Timer_Tick event handler, add the current time and the feedback object into the Tag property of the timer for later use, start the timer, and add it into the timers collection.
In the Timer_Tick method, we access the timer from the sender input parameter, and the Feedback instance from its Tag property. The feedback item is then removed from the Feedback collection, the timer is stopped and removed from the timers collection, and the Tick event handler is detached.
In the DeleteButton_PreviewMouseLeftButtonDown method, we first cast the delete button from the sender input parameter. We then cast the Feedback object from the button's DataContext property and remove it from the Feedback collection.
In the Border_MouseEnter method, we iterate through the timers collection and stop each timer. The interval of each timer and duration of each associated Feedback object is then set to the remaining time that they should be displayed for, in effect, pausing their durations.
Finally, we see the Border_MouseLeave method, which re-initializes the Tag property of each timer in the timers collection, with the same feedback item and the current date and time, and restarts it when the user's mouse pointer leaves the feedback control.
This means that the length of time that temporary feedback messages are displayed can be extended if the user moves their mouse pointer over the feedback control. This feature will hold the feedback messages in the control for as long as the user keeps their mouse pointer over the control, giving them ample time to read the messages. Let's now see what this control looks like:

If you have menu buttons at the top of your Views, then you could alternatively have the feedback appear at the bottom of the application, or even sliding in from one of the sides. Also note that the delete buttons have not been styled, so as to shorten this example, but they should be styled in line with the other controls in a real application.
If you remember from Chapter 3, Writing Custom Application Frameworks, all of our View Models will have access to our new FeedbackManager class through the FeedbackManager property in our BaseViewModel class, and so we can replicate the feedback in the preceding image from any View Model like this:
FeedbackManager.Add(new Feedback("Here's some information for you", FeedbackType.Information)); FeedbackManager.Add("Something was saved successfully", true); FeedbackManager.Add("Something else went wrong", false); FeedbackManager.Add("Something else went wrong too", false);
Let's now move on to discover how we can make our applications more responsive by maximizing the utilization of the CPU.