Smart Notifier for Executables | Toast for .NET
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.
- Download WPF demo - 23 KB
- Download demo - 20.1 KB
- Download WPF_Binary_v4.zip - 17.4 KB
- Download Binary_v4.zip - 14.8 KB
- Download source - 243.9 KB
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:
Warning
Ok
Error
Info
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:
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:
#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...
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
.
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:
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.
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:
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:
- ADJUST STYLE
- ADJUST LOCATIONS
- 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:
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:
- First, we get the screen area and calculate the available grid dimension (row and columns).
- 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).
- 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)
{
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:
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:
#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
:
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:
- 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 theSimpleLogger
- 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