Click here to Skip to main content
13,626,866 members
Click here to Skip to main content
Add your own
alternative version

Stats

24.1K views
2.2K downloads
86 bookmarked
Posted 25 Aug 2016
Licenced CPOL

Smart Notifier for Executables | Toast for .NET

, 12 Jan 2018
Rate this:
Please Sign up or sign in to vote.
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.

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:

// 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:

using WPFNotify;

Then, to create a note, simply:

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:

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:

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

Briefly:

// 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:

// Dialog style of black background
public enum BackDialogStyle {None, FadedScreen, FadedApplication }

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

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

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

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

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

// Base Notifier data
private short ID = 0;
private String description = "";
private String title = "Notifier";
private NoteLocation location;
private Type type = Type.INFO;

// Dialog data part
private bool isDialog = false;
private BackDialogStyle backDialogStyle = BackDialogStyle.None;
private Form myCallerApp;

// Temporary note part
private int timeout_ms = 0;
private AutoResetEvent timerReset = null;
#endregion

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

The...

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

    // Mouse bar drag
    internal Point initialLocation;
    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.

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

CREATE & DRAW

In the OnLoad method, called on the form creation:

BackColor = Color.Blue;
TransparencyKey = Color.Blue;
FormBorderStyle = FormBorderStyle.None;

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

this.Tag = "__Notifier|"+ ID.ToString("X4");

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:

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

    // Fill the Notifier data
    titleText.Text = title;
    labelDesc.Text = description;
    date.Text = DateTime.Now + "";
    //idLabel.Text = ID.ToString("0000");

    #region ADJUST COLORS
    Color Hover = Color.FromArgb(0, 0, 0);
    Color Leave = Color.FromArgb(0, 0, 0);

    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;
    }

    // Init color
    buttonClose.BackColor = Leave;
    buttonMenu.BackColor = Leave;
    titleText.BackColor = Leave;

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

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

    if (isDialog)
    {
        // Add Buttons
        Button ok_button = new Button();
        ok_button.FlatStyle = FlatStyle.Flat;
        ok_button.BackColor = Leave;
        ok_button.ForeColor = Color.White;
        this.Size = new Size(this.Size.Width, this.Size.Height + 50);
        ok_button.Size = new Size(120, 40);
        ok_button.Location = new Point
           (this.Size.Width / 2 - ok_button.Size.Width / 2, this.Size.Height - 50);
        ok_button.Text = DialogResult.OK.ToString();
        ok_button.Click += ok_button_Click;
        this.Controls.Add(ok_button);

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

    #region ADJUST LOCATION
    if (!isUpdate)
    {
        Rectangle rec = Screen.GetWorkingArea(Screen.PrimaryScreen.Bounds);

        // Add position
        // Check for a available Position
        int maxNot = rec.Height / this.Height;
        int x_Pos = rec.Width - this.Width;

        //int x_Shift = this.Width + 5;     // Full visible note (no overlay)
        int x_Shift = 25;                   // Custom overlay
        int n_columns = 0;
        int n_max_columns = rec.Width / x_Shift;
        bool add = false;
        if (!isDialog)
        {
            location = new NoteLocation(x_Pos, rec.Height - (this.Height * 1));

            while (!add)
            {
                for (int n_not = 1; n_not <= maxNot; n_not++)
                {
                    location = new NoteLocation(x_Pos, rec.Height - (this.Height * n_not));

                    if (!checkLocationUsed(location))
                    {
                        add = true; break;
                    }

                    // X shift if no more column space
                    if (n_not == maxNot)
                    {
                        n_columns++;
                        n_not = 0;
                        x_Pos = rec.Width - this.Width - x_Shift * n_columns;
                    }

                    // Last exit condition: the screen is full of note
                    if (n_columns >= n_max_columns)
                    {
                        add = true; break;
                    }
                }
            }
        }
        else
        {
            // Positioning
            switch (backDialogStyle)
            {
                case BackDialogStyle.FadedScreen:
                case BackDialogStyle.None:
                    // Center Screen position
                    int X = 0, Y = 0;
                    X = (rec.Width - this.Width) / 2;
                    Y = (rec.Height - this.Height) / 2;
                    location = new NoteLocation(X, Y);
                    break;
                case BackDialogStyle.FadedApplication:
                    // Center of myCallerApp
                    int px = myCallerApp.Location.X + myCallerApp.Size.Width / 2;
                    int py = myCallerApp.Location.Y + myCallerApp.Size.Height / 2;
                    px = px - this.Width / 2;
                    py = py - this.Height / 2;
                    location = new NoteLocation(px, py);
                    break;
            }
        }

        // Notifier position
        this.Location = new Point(location.X, location.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:

#region ADJUST LOCATION
if (!isUpdate)
{
    if (!isDialog)
    {
        Rectangle rec = Screen.GetWorkingArea(Screen.PrimaryScreen.Bounds);

        // Add position
        // Check for a available Position
        int maxNot = rec.Height / this.Height;
        int x_Pos = rec.Width - this.Width;

        //int x_Shift = this.Width + 5;     // Full visible note (no overlay)
        int x_Shift = 25;                   // Custom overlay
        int n_columns = 0;
        int n_max_columns = rec.Width / x_Shift;
        bool add = false;
        location = new NoteLocation(x_Pos, rec.Height - (this.Height * 1));

        while (!add)
        {
            for (int n_not = 1; n_not <= maxNot; n_not++)
            {
                location = new NoteLocation(x_Pos, rec.Height - (this.Height * n_not));

                if (!checkLocationUsed(location))
                {
                    add = true; break;
                }

                // X shift if no more column space
                if (n_not == maxNot)
                {
                    n_columns++;
                    n_not = 0;
                    x_Pos = rec.Width - this.Width - x_Shift * n_columns;
                }

                // Last exit condition: the screen is full of note
                if (n_columns >= n_max_columns)
                {
                    add = true; break;
                }
            }
        }
    }
    else
    {
        // Positioning
        switch (backDialogStyle)
        {
            case BackDialogStyle.FadedScreen:
            case BackDialogStyle.None:
                // Center Screen position
                int X = 0, Y = 0;
                X = (rec.Width - this.Width) / 2;
                Y = (rec.Height - this.Height) / 2;
                location = new NoteLocation(X, Y);
                break;
            case BackDialogStyle.FadedApplication:
                // Center of myCallerApp
                int px = myCallerApp.Location.X + myCallerApp.Size.Width / 2;
                int py = myCallerApp.Location.Y + myCallerApp.Size.Height / 2;
                px = px - this.Width / 2;
                py = py - this.Height / 2;
                location = new NoteLocation(px, py);
                break;
        }
    }

    // Notifier position
    this.Location = new Point(location.X, location.Y);
}
#endregion

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:

public static void Update(short ID, string desc, Type noteType, string title)
{
    for (int i = 0; i < notes.Count; i++)
    {
        // Get the node
        if (notes[i].Tag != null &&
            notes[i].Tag.Equals("__Notifier|" + ID.ToString("X4")))
        {
            // Reset the timeout timer (if any)
            if (notes[i].timerReset != null)
                notes[i].timerReset.Set();

            Notifier myNote = (Notifier)notes[i];
            myNote.setNotifier(desc, noteType, title, true);
        }
    }
}

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:

if (not.timeout_ms >= 500)
{
    not.timerReset = new AutoResetEvent(false);
    BackgroundWorker timer = new BackgroundWorker();
    timer.DoWork += timer_DoWork;
    timer.RunWorkerCompleted += timer_RunWorkerCompleted;

    timer.RunWorkerAsync(not);
}

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

#region TIMER
 private static void timer_DoWork(object sender, DoWorkEventArgs e)
 {
     Notifier not = (Notifier)e.Argument;
     bool timedOut = false;
     while (!timedOut)
     {
         if (!not.timerReset.WaitOne(not.timeout_ms))
         {
             // Time is out
             timedOut = true;
         }
     }
     e.Result = e.Argument;
 }
 private static void timer_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
 {
     Notifier not = (Notifier) e.Result;
     not.closeMe();
 }
 #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.

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:

Logger logger = new Logger();

...or specify a filename:

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.

logger.setLoggingLevel(Logger.LEVEL.INFO);

Use it:

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

Points of Interest

Some of the future developments are as follows:

  • Dock the note to the application instead of screen - keep the position on parent dragging
  • Dialog style note: complete version (WPF)
  • Configurable position of the note (X, Y, Corner of the screen)
  • Complete multiscreen support
  • Restyling of the WPF code according to MVVM
  • WPF: some effects (es: fade)
  • Logger: log to console

History

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

Share

About the Author

boschi84
Italy Italy
No Biography provided

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 3 Pin
Marcin Zamorski5-Jan-18 0:18
memberMarcin Zamorski5-Jan-18 0:18 
QuestionNice Article Pin
saineshwar bageri4-Jan-18 19:33
membersaineshwar bageri4-Jan-18 19:33 
QuestionOld code. Pin
Emil Steen4-Jan-18 4:43
memberEmil Steen4-Jan-18 4:43 
QuestionWhy did you do it with Windows Forms? Pin
s0m0s4-Jan-18 4:40
members0m0s4-Jan-18 4:40 
GeneralMy vote of 5 Pin
Carlven Lao4-Jan-18 3:45
memberCarlven Lao4-Jan-18 3:45 
Questionwhat's been changed/addes since October, 2017 ? Pin
BillWoodruff3-Jan-18 22:39
mentorBillWoodruff3-Jan-18 22:39 
QuestionAuto-Close & User Interaction Pin
tolsen6427-Jul-17 11:54
membertolsen6427-Jul-17 11:54 
AnswerRe: Auto-Close & User Interaction Pin
boschi847-Sep-17 11:56
memberboschi847-Sep-17 11:56 
QuestionAutoclose Pin
Daniel Santillanes27-Jul-17 10:14
professionalDaniel Santillanes27-Jul-17 10:14 
AnswerRe: Autoclose Pin
boschi847-Sep-17 11:30
memberboschi847-Sep-17 11:30 
AnswerRe: Autoclose Pin
Member 107860005-Jan-18 4:57
memberMember 107860005-Jan-18 4:57 
GeneralRe: Autoclose Pin
Daniel Santillanes6-Jan-18 19:59
professionalDaniel Santillanes6-Jan-18 19:59 
GeneralRe: Autoclose Pin
boschi847-Jan-18 12:00
memberboschi847-Jan-18 12:00 
GeneralMy vote of 5 Pin
Carlven Lao12-Jul-17 4:53
memberCarlven Lao12-Jul-17 4:53 
GeneralRe: My vote of 5 Pin
boschi8414-Jul-17 2:46
memberboschi8414-Jul-17 2:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web03-2016 | 2.8.180712.1 | Last Updated 12 Jan 2018
Article Copyright 2016 by boschi84
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid