Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Smart Notifier for Executables | Toast for .NET

4.62/5 (53 votes)
4 Aug 2018CPOL7 min read 54.3K   4.5K  
A quite simple notifier (a MessageBox substitute) to get a better message representation of messages in an executable. The idea is to get a solution similar to the Toast, from Android, where the notifications are not invasive.It works for Windows Form and WPF.

 

Introduction

The aim of this library is to provide a new and simple solution to represent the notifier message of an application, instead of using the classic MessageBox.

MessageBox is a very powerful message notifier, but it shows a lack of style and practice. A lot of customers require a UI that is in line with today's market. This idea was born in 2008, during a University work: from the v3 of this library is published a WPF version also, which is only a simple porting from the Windows Form (.NET 2.0) version (thus it needs more time to be concluded). However you can use it, especially for debug purposes of your app.

Another aim is to get a notification that is easy to update in its content: sometimes, it is necessary to put a MessageBox in a loop, to get (for instance) a fast error report. This often causes a waterfall of message notifications. In this case, we need a notification that can stay opened, on top, in a comfortable position and with a updatable content, thus it is possible to update a hypothetical error counter.

The Features

  • Up to four simple notifier types with respective colors:
    • Info
    • Error
    • Warning
    • Confirm
  • Draggable Notifier
  • DialogStyle Notifier
  • Positionable Notifier
  • Message with the same content/title are now handled as multi-notes: you will see an update counter in the title of the note
  • Full Screen faded background (experimental)
  • Added SimpleLogger: a custom logger to VS Console or File

Using the Code

To use this code, simply insert a reference to the Notifier.dll library to your project:

C#
// Common
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;

// 1. Add the NS of the notifier
using Notify;

To use the WPF version, use:

C#
using WPFNotify;

Then, to create a note, simply:

C#
Notifier.Show("Note text", Notifier.Type.INFO, "TitleBar");

You have to indicate the text of the note, the type and the title. Also, it is possible to use a simple call, with only the text ("Info" as the default type).

You can choose between 4 styles:

  1. Warning
  2. Ok
  3. Error
  4. Info

preview

The notifications are stacked to the right bottom corner of the active screen.

In the above image, you can see a key-feature of this notification library: you can now handle the same note as one note with a counter updates.

Also, the creation call returns the ID of the note. We can use this ID to change the notification content:

C#
short ID = Notifier.Show("Note text", Notifier.Type.INFO, "TitleBar");

Then, it is possible to use this ID to refer to the opened notification to update its content:

C#
Notifier.Update(ID, "New Note text", Notifier.Type.OK, "New TitleBar");

Briefly:

C#
// Create a simple note
short ID = Notifier.Show("Hello World");

// Change the created note
Notifier.Update(ID, "Hello Mars");

It also introduced the "Close All" function: to access it, open the notification menu by pressing the menu icon left to the close icon:

menu icon

In the opened menu, select "Close All" to close all the opened notifications; this will also reset the ID counter.

Watching the Code

The Notifier.dll library is made of a Windows Form and its resource file. In the resource file are stored the background image of the note and the icons.

The WPFNotifier.dll library is made of a Windows Presentation Foundation and its resource file. In the resources are stored the background image of the note and the icons.

The below code descriptions refer to the Windows Form version.

GLOBALS

Let's start from the GLOBALS part:

C#
#region GLOBALS      
        public enum Type { INFO, WARNING, ERROR, OK }                   // Set the type of the Notifier

        class NoteLocation                                              // Helper class to handle Note position
        {
            internal int X;
            internal int Y;

            internal Point initialLocation;                             // Mouse bar drag helpers
            internal bool mouseIsDown = false;

            public NoteLocation(int x, int y)
            {
                this.X = x;
                this.Y = y;
            }
        }

        static List<Notifier> notes             = new List<Notifier>(); // Keep a list of the opened Notifiers

        private NoteLocation noteLocation;                              // Note position
        private short ID                        = 0;                    // Note ID
        private string description              = "";                   // Note default Description
        private string title                    = "Notifier";           // Note default Title
        private Type type                       = Type.INFO;            // Note default Type

        private bool isDialog = false;                                  // Note is Dialog
        private BackDialogStyle backDialogStyle = BackDialogStyle.None; // DialogNote default background
        private Form myCallerApp;                                       // Base Application for Dialog Note

        private Color Hover = Color.FromArgb(0, 0, 0, 0);               // Default Color for hover
        private Color Leave = Color.FromArgb(0, 0, 0, 0);               // Default Color for leave

        private int timeout_ms                  = 0;                    // Temporary note: timeout
        private AutoResetEvent timerResetEvent  = null;                 // Temporary note: reset event

        private Form inApp = null;                                      // In App Notifier: the note is binded to the specified container
#endregion

That part includes the helper class used to save the position of the note and its information.

The...

C#
        class NoteLocation                                              // Helper class to handle Note position
        {
            internal int X;
            internal int Y;

            internal Point initialLocation;                             // Mouse bar drag helpers
            internal bool mouseIsDown = false;

            public NoteLocation(int x, int y)
            {
                this.X = x;
                this.Y = y;
            }
        }

...is used to store the positions of all the notifications, because every time a note is created, it is necessary to check the available position to show it in an empty space. To save the notifications, we use a static container and a static ID counter to be sure that each Notifier has its ID.

C#
        static List<Notifier> notes             = new List<Notifier>(); // Keep a list of the opened Notifiers

CREATE & DRAW

In the OnLoad method, called on the form creation:

C#
            BackColor       = Color.Blue;                               // Initial default graphics 
            TransparencyKey = Color.FromArgb(128, 128, 128);            // Initial default graphics 
            FormBorderStyle = FormBorderStyle.None;                     // Initial default graphics 

We set the transparency of the form, so we can use a custom background to achieve a better smooth effect and a transparent border.

C#
            this.Tag = "__Notifier|" + ID.ToString("X4");               // Save the note identification in the Tag field

            setNotifier(description, type, title);  

This TAG is used to identify the notification type of the form. It is helpful for the "Update" operation.

To set the content of the note, this function is used:

C#
        private void setNotifier(string description, 
                                 Type noteType, 
                                 string title, 
                                 bool isUpdate = false)
        {
            this.title          = title;
            this.description    = description;
            this.type           = noteType;

            noteTitle.Text      = title;                                // Fill the Notifier data title
            noteContent.Text    = description;                          // Fill the Notifier data description
            noteDate.Text       = DateTime.Now + "";                    // Fill the Notifier data Timestamp

#region ADJUST COLORS
            switch (noteType)
            {
                case Type.ERROR:
                    icon.Image = global::Notify.Properties.Resources.ko;
                    Leave = Color.FromArgb(200, 60, 70);
                    Hover = Color.FromArgb(240, 80, 90);
                    break;
                case Type.INFO:
                    icon.Image = global::Notify.Properties.Resources.info;
                    Leave = Color.FromArgb(90, 140, 230);
                    Hover = Color.FromArgb(110, 160, 250);
                    break;
                case Type.WARNING:
                    icon.Image = global::Notify.Properties.Resources.warning;
                    Leave = Color.FromArgb(200, 200, 80);
                    Hover = Color.FromArgb(220, 220, 80);
                    break;
                case Type.OK:
                    icon.Image = global::Notify.Properties.Resources.ok;
                    Leave = Color.FromArgb(80, 200, 130);
                    Hover = Color.FromArgb(80, 240, 130);
                    break;
            }

            buttonClose.BackColor = Leave;                              // Init colors
            buttonMenu.BackColor  = Leave;
            noteTitle.BackColor   = Leave;

            this.buttonClose.MouseHover += (s, e) =>                    // Mouse hover
            {
                this.buttonClose.BackColor = Hover;
                this.buttonMenu.BackColor = Hover;
                this.noteTitle.BackColor = Hover;
            };
            this.buttonMenu.MouseHover += (s, e) =>
            {
                this.buttonMenu.BackColor = Hover;
                this.buttonClose.BackColor = Hover;
                this.noteTitle.BackColor = Hover;
            }; this.noteTitle.MouseHover += (s, e) =>
            {
                this.buttonMenu.BackColor = Hover;
                this.buttonClose.BackColor = Hover;
                this.noteTitle.BackColor = Hover;
            };

            this.buttonClose.MouseLeave += (s, e) =>                    // Mouse leave
            {
                this.buttonClose.BackColor = Leave;
                this.buttonMenu.BackColor = Leave;
                this.noteTitle.BackColor = Leave;
            };
            this.buttonMenu.MouseLeave += (s, e) =>
            {
                this.buttonMenu.BackColor = Leave;
                this.buttonClose.BackColor = Leave;
                this.noteTitle.BackColor = Leave;
            };
            this.noteTitle.MouseLeave += (s, e) =>
            {
                this.buttonMenu.BackColor = Leave;
                this.buttonClose.BackColor = Leave;
                this.noteTitle.BackColor = Leave;
            };
#endregion

#region DIALOG NOTE
            if (isDialog)
            {
                Button ok_button    = new Button();                     // Dialog note comes with a simple Ok button
                ok_button.FlatStyle = FlatStyle.Flat;
                ok_button.BackColor = Leave;
                ok_button.ForeColor = Color.White;
                Size                = new Size(Size.Width,              // Resize the note to contain the button
                                               Size.Height + 50);
                ok_button.Size      = new Size(120, 40);
                ok_button.Location  = new Point(Size.Width / 2 - ok_button.Size.Width / 2, 
                                                Size.Height - 50);
                ok_button.Text      = DialogResult.OK.ToString();
                ok_button.Click     += onOkButtonClick;
                Controls.Add(ok_button);

                noteDate.Location   = new Point(noteDate.Location.X,    // Shift down the date location
                                                noteDate.Location.Y + 44); 


                noteLocation        = new NoteLocation(Left, Top);      // Default Center Location
            }
#endregion

#region NOTE LOCATION
            if (!isDialog && !isUpdate)
            {
                NoteLocation location = adjustLocation(this);           // Set the note location

                Left = location.X;                                      // Notifier position X    
                Top  = location.Y;                                      // Notifier position Y 
            }
#endregion
        }

That set all the notification elements with the desired content. We have to handle different things:

  1. ADJUST STYLE
  2. ADJUST LOCATIONS
  3. HANDLE THE DIALOG STYLE NOTE

Dialog style is quite different from the simple docked notification: it requires a button to check the user event acquire as well as the close button. Also, the dialog style locks the application GUI until the note is closed.

Optionally, it will have a faded black background (which is currently not possible with a simple messageBox) over the application or over the whole screen (try it in the demo).

In the #region ADJUST LOCATION, we find a place for our notification:

C#
        private NoteLocation adjustLocation(Notifier note)
        {
            Rectangle notesArea;
            int nMaxRows    = 0, 
                nColumn     = 0,
                nMaxColumns = 0,
                xShift      = 25;                                                     // Custom note overlay
            //  x_Shift     = this.Width + 5;                                         // Full visible note (no overlay)
            bool add = false;

            if (inApp != null && inApp.WindowState ==  FormWindowState.Normal)        // Get the available notes area, based on the type of note location
            {
                notesArea = new Rectangle(inApp.Location.X, 
                                          inApp.Location.Y, 
                                          inApp.Size.Width, 
                                          inApp.Size.Height);
            }
            else
            {
                notesArea = new Rectangle(Screen.GetWorkingArea(note).Left,
                                          Screen.GetWorkingArea(note).Top,
                                          Screen.GetWorkingArea(note).Width,
                                          Screen.GetWorkingArea(note).Height);
            }

            nMaxRows    = notesArea.Height / Height;                                  // Max number of rows in the available space
            nMaxColumns = notesArea.Width  / xShift;                                  // Max number of columns in the available space

            noteLocation = new NoteLocation(notesArea.Width  +                        // Initial Position X                                        
                                            notesArea.Left   -
                                            Width,
                                            notesArea.Height +                        // Initial Position Y
                                            notesArea.Top    -
                                            Height);

            while (nMaxRows > 0 && !add)                                              // Check the latest available position (no overlap)
            {
                for (int nRow = 1; nRow <= nMaxRows; nRow++)
                {
                    noteLocation.Y =    notesArea.Height +
                                        notesArea.Top    -
                                        Height * nRow;

                    if (!isLocationAlreadyUsed(noteLocation, note))
                    {
                        add = true; break;
                    }

                    if (nRow == nMaxRows)                                            // X shift if no more column space
                    {
                        nColumn++;
                        nRow = 0;

                        noteLocation.X =  notesArea.Width           +
                                          notesArea.Left            - 
                                          Width - xShift * nColumn;
                    }

                    if (nColumn >= nMaxColumns)                                      // Last exit condition: the screen is full of note
                    {
                        add = true; break;
                    }
                }
            }

            noteLocation.initialLocation = new Point(noteLocation.X,                  // Init the initial Location, for drag & drop
                                                     noteLocation.Y);             
            return noteLocation;
        }

First, we check if is an update of the note: in this case, it is not needed to evaluate a position because the updated note already is displayed.

Then, we see if it's a dialog note: the dialog uses the location defined in the bottom part, while the docked note uses the first branch:

  1. First, we get the screen area and calculate the available grid dimension (row and columns).
  2. Then, we start the position cycle: if a column is full, we shift the notification across the X axis of the screen by a custom value (with or not notifications overlay - uncomment the line if you do not want overlay).
  3. If the entire screen is full, then create the notifications in the last used place.

In the end, we set the note position.

Let's see the interesting part, the Update function.

UPDATE

To update the notification, as previously said, we need its ID.

With the note ID, it is simple to find the needed notification and update it:

C#
        public static void Update(short ID, 
                                  string desc, 
                                  Type noteType, 
                                  string title)
        {
            foreach (var note in notes)
            {
                if (note.Tag != null &&                                     // Get the node
                    note.Tag.Equals("__Notifier|" + ID.ToString("X4")))
                {
                    if (note.timerResetEvent != null)                            // Reset the timeout timer (if any)
                        note.timerResetEvent.Set();

                    Notifier myNote = (Notifier)note;
                    myNote.setNotifier(desc, noteType, title, true);        // Set the new note content
                }
            }
        }       

We cycle in the notifications list and check for the ID set in the TAG properties: then is called the setNotifier part that handles the update changing the note content (or type).

TEMPORARY NOTE

A very useful function: we can create a note and tell if it's an autoclose note or not. I use this for some volatile information.

Technically, this is achieved using a BackgroundWorker and a AutoResetEvent In the Show call, we define it:

C#
                if (not.timeout_ms >= 500)                                          // Start autoclose timer (if any)
                {
                    not.timerResetEvent      = new AutoResetEvent(false);

                    BackgroundWorker timer   = new BackgroundWorker();
                    timer.DoWork             += timer_DoWork;
                    timer.RunWorkerCompleted += timer_RunWorkerCompleted;
                    timer.RunWorkerAsync(not);                                      // Timer (temporary notes)
                }

Then, it is easy to close not after the specified ms timeout is elapsed:

C#
#region TIMER
        //-------------------------------------------------------------------------------------------------------------------------------
        //                                  Background Worker to handle the timeout of the note
        //-------------------------------------------------------------------------------------------------------------------------------
        private static void timer_DoWork(object sender, DoWorkEventArgs e)
        {
            Notifier not = (Notifier)e.Argument;
            bool timedOut = false;
            while (!timedOut)
            {
                if (!not.timerResetEvent.WaitOne(not.timeout_ms))
                    timedOut = true;                                        // Time is out
            }
            e.Result = e.Argument;
        }

        //-------------------------------------------------------------------------------------------------------------------------------
        //                                  Background Worker to handle the timeout event
        //-------------------------------------------------------------------------------------------------------------------------------
        private static void timer_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            Notifier not = (Notifier) e.Result;
            not.closeMe();                                                  // Close the note
        }
#endregion

The background worker handles the background part of the timeout: the timeout event is an auto reset event: it is useful for the update part. Let's consider a note that displays the same message, as we know, it will be handled using the note counter: but what if this note is a temp note? For every new notification the timer is reset, so the note can display at least the counter for the desired times.

IN APP NOTE

The new feature introduced in the v4 release (for both WinForm and WPF) is the ability to insert the note snapped to the container application. You can try it in the demo to better understand this feature. Usually the notes are snapped to the bottom right corner of the screen, but if you enable the IN APP feature you can snap the notes to the bottom right corner of a specified Form or Window.

Simple Logger

To get more power regarding the notification to the user, it is possible to use the included logger, called SimpleLogger.

A very basilar logger for .NET. It is possible to choose a filename for the log and the level of logging message.

In your class, include the logger:

C#
Logger logger = new Logger();

...or specify a filename:

C#
Logger logger = new Logger("myManager.log");

OPTIONAL: Set the log level (Remember: CRITICAL < ERROR < WARNING < INFO < VERBOSE) so if you set the level to WARNING, you will have in the log the CRITICAL, ERROR and WARNING if you set the level to CRITICAL, you will have only CRITICAL in the log Default value is VERBOSE.

C#
logger.setLoggingLevel(Logger.LEVEL.INFO);

Use it:

C#
logger.log(Logger.LEVEL.ERROR, e.StackTrace);

Points of Interest

Some of the future developments are as follows:

  • Configurable startup position of the note (X, Y, Corner of the screen)
  • WPF: some effects (es: fade)
  • Fix the redraw procedure that causes multiple redraw for each mouse drag
  • Logger: log to console

History

  • 05/08/2018: v1.4 - Added the InApp features and a better multi screen support (WinForm and WPF)
  • 12/01/2018: v1.3 - Added the WPF version - added the temporary note support
  • 10/07/2017: v1.2 - Added the features as showDialog, draggable Notifier. Improved the graphics part, update counters of the same note. Added the SimpleLogger
  • 01/09/2016: v1.1 - Changed the caller style in a static way, added the Close All function and the notification ID: it is possible now to recall and change the content of an opened notification
  • 25/08/2016: v1.0 - First version release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)