using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.ComponentModel;
using System.IO;
using Subro.Exceptions;
namespace Subro.Interaction
{
///
<summary>
/// A single feedback item. A feedback item can provide information
/// on status, progress and datetimes of execution
/// </summary>
public class Feedback{
public Feedback(
string Message):
this()
{
this.message = Message;
}
public Feedback()
{this.lastupdate = this.DateTime;
}
/// <summary>
/// The time the feedback object was created
/// </summary>
public readonly DateTime DateTime = DateTime.Now;
DateTime lastupdate;
/// <summary>
/// The last time the Feedback was updated (for example by updating the progress)
/// </summary>
public DateTime LastUpdate
{get { return lastupdate; }
}private
void SetLastUpdate()
{
lastupdate = DateTime.Now;
}
///
<summary>
/// The main information of this feedback
/// </summary>
string message;
public string Message
{
get {
return message; }
set{
message = value;
NotifyUpdate();
}
}int progress;
/// <summary>
/// The progress of this item. The total progress is in relation
/// to the the value set at <see cref="ProgressTarget"/>
/// <seealso cref="ProgressTarget"/>
/// <see cref="ProgressPercentage"/>
/// </summary>            
public int Progress
{
get {
return progress; }
set{
progress = value;
IsProgressUpdate = true;
NotifyUpdate();
}
}public event EventHandler Updated;
public
void NotifyUpdate()
{
SetLastUpdate();
if (Updated !=
null)
Updated(this, EventArgs.Empty);
}
int progresstarget = 100;
bool progressinpercentage =
true;
/// <summary>
/// Normal target of process is 100 as in 100 percent. Whatever value this
/// is set to, progress relates to it.
/// For example: if looping through a number of lines,
/// you can set this target to the number of lines and <see cref="Progress"/> to
/// the line being processed
/// </summary>
public int ProgressTarget
{
get {
return progresstarget; }
set{
progresstarget = value;
progressinpercentage = false;
NotifyUpdate();
}
}///
<summary>
/// The relative progress of this item.
/// </summary>
public float ProgressPercentage
{
get
{
if (!IsProgressUpdate) return 1;
return (float)progress / progresstarget;
} }public
string GetProgressString()
{
if (progressinpercentage)
return ProgressPercentage.ToString("###%");
return progress.ToString() +
"/" + progresstarget.ToString();
}///
<summary>
/// Increase the <see cref="Progress"/> by one
/// </summary>
public void Increase()
{
Progress++;
}
public
void ShowAlive()
{
IsAliveIndicator = true;
}
public bool IsProgressUpdate =
false;
public int alivecount;
public int AliveCount {
get {
return alivecount; } }
public bool IsAliveIndicator
{
get {
return alivecount > 0; }
set{
if (
value)
{
if (++alivecount == 0)
alivecount++;
NotifyUpdate();
}
elsealivecount = 0;
} }
public override string ToString()
{return string.Format("{0}\t{1}", DateTime, message);
}///
<summary>
/// The level, this can be a custom level or set with <see cref="FeedbackLevel"/>.
/// 0 = normal  negative value is not so important, positive value is more important
/// </summary>
public int Level = (
int)
FeedBackLevel.Normal;
public FeedBackLevel FeedbackLevel
{
get
{
try
{
return (FeedBackLevel)Level;
}
catch
{return FeedBackLevel.Custom;
} }
set
{Level = (int)value;
} }public bool IsNormalLevel { get { return Level == (int)FeedBackLevel.Normal; } }
public
virtual bool CanShowExtraInfo
{
get { return HasChildren; }
}
public void ShowExtraInfo()
{ShowExtraInfo(null);
}
public virtual void ShowExtraInfo(IWin32Window Owner)
{children.Show(Owner);
}
protected virtual void AppendExtraInfo(AppendInfoSettings info)
{children.AppendFeedbackText(info);
}public
virtual FeedbackItemPainter GetPainter(
FeedbackItemInfo info)
{
return new FeedbackItemPainter(info);
}internal
void AppendInfo(
AppendInfoSettings info,
int RowNumber)
{
var sb = info.StringBuilder;
sb.Append(
"<DIV>");
bool bold = Level > 0, smaller = Level < 0;
if (bold) sb.Append(
"<B>");
else if (smaller) sb.Append(
"<span style='font-size:smaller'>");
sb.Append(
"<span style='font-size:smaller;font-style:italic;'>").Append(DateTime)
.Append("</span>");
if (IsProgressUpdate)
{
int p = (
int)(ProgressPercentage * 100);
sb.Append(
@"<span style=""border:'1 solid black';width:75px;margin-left:10;font-size:smaller"">")
.Append("<span style='position:absolute;width:")
.Append(p)
.Append("%;background-color:green'></span>")
.Append("<span style='position:relative;width:100%'>")
.Append(GetProgressString()).Append("</span>")
.Append("</span>");
}
sb.Append("<span style='margin-left:10'>").Append(message).Append("</span>");
if (CanShowExtraInfo)
{
string ExtraInfoAnchor =
"ExtraInfo" + RowNumber.ToString();
if (info.AllowScripts)
{
sb.Append(@"<A style='margin-left:5' href='javascript:' onclick=""
this.index=1-this.index;
varindex = this.index,
modes=new Array('none',''),
texts=new Array('Show details','Hide details');
this.nextSibling.style.display=modes[index];
this.innerText=texts[index];""
index=1>Hide details</A>");}
sb.Append(
"<div style='margin-left:20;background-color:lightblue;'>")
.Append("<a name='").Append(ExtraInfoAnchor).Append("'></a>");
AppendExtraInfo(info);
sb.Append(
"</div>");
}
if (bold) sb.Append("</B>");
else if (smaller) sb.Append("</span>");
sb.Append("</DIV>");
}
FeedbackCollection children;
/// <summary>
/// Use this colleciton to add children. NB calling this function the first time will
/// create the collection
/// </summary>
public FeedbackCollection Children
{
get
{
if (children ==
null)
{
children = new FeedbackCollection();
children.Parent = this;
}
return children;
} }public
bool HasChildren
{
get { return children != null && children.Count > 0; }
}
#region static
#region mail
public
delegate void SendMailDelegate(
string Subject,
string Body,
bool IsHTML);
static SendMailDelegate sendmail =
delegate(
string Subject,
string Body,
bool IsHTML)
{
System.Net.Mail.MailMessage sm = new System.Net.Mail.MailMessage();
sm.IsBodyHtml = IsHTML;
sm.Body = Body;
sm.Subject = Subject;
System.Net.Mail.SmtpClient smtp = new System.Net.Mail.SmtpClient();
smtp.Send(sm);
};
public static SendMailDelegate DefaultSendMailMethod
{
get {
return sendmail; }
set{
if (
value ==
null)
throw new ArgumentNullException();
sendmail = value; } }#endregion
#region Exceptions
public delegate void ShowExceptionDelegate(
Exception ex);
static ShowExceptionDelegate showex =
delegate(
Exception ex)
{
string m = ex.Message +
"\r\n----------------\r\nStack:\r\n"+ ex.StackTrace;
MessageBox.Show(m);
}; public static ShowExceptionDelegate DefaultShowExceptionMethod
{
get {
return showex; }
set{
if (value == null) throw new ArgumentNullException();
showex = value;
} }
#endregionpublic
static implicit operator Feedback(
string value)
{
return new Feedback(value);
}#endregion
}
///
<summary>
/// General settings when the text of feedback is obtained.
/// <seealso cref="FeedbackCollection.GetTotalFeedbackText"/>
/// <see cref="Feedback.AppendInfo"/>
/// </summary>
public class AppendInfoSettings{
public AppendInfoSettings():
this(
new StringBuilder())
{
}
public AppendInfoSettings(
StringBuilder sb)
{
this.StringBuilder = sb;
}
public readonly StringBuilder StringBuilder;
public bool AllowScripts;
public int MaxLines;public
AppendInfoSettings Append(
string Value)
{
StringBuilder.Append(Value);
return this;
}
public
AppendInfoSettings Append<T>(T Value)
{
StringBuilder.Append(Value);
return this;
}
}
///
<summary>
/// A collection of <see cref="Feedback"/> items. Besides being a collection
/// for different Feedback items, this class contains the methods to
/// visualize the feedback
/// </summary>
public class FeedbackCollection :
BindingList<
Feedback>
{
public FeedbackCollection() { }
public FeedbackCollection(
int MaximumValues)
{
maxcount = MaximumValues;
}
/// <summary>
/// This value can be set to indicate that this set is a childset of
/// another Feedback item
/// </summary>
public Feedback Parent;
#region Add/Update
protected
override void InsertItem(
int index,
Feedback fb)
{
base.InsertItem(index, fb);
CheckMaxValues();            
}protected
override void OnListChanged(
ListChangedEventArgs e)
{
base.OnListChanged(e);
if (Parent !=
null)
Parent.NotifyUpdate();
}
public
Feedback Add(
string Text)
{
Feedback fb = new Feedback(Text);
Add(fb);
return fb;
}
public Feedback Add(string Text, FeedBackLevel Level)
{Feedback fb = new Feedback(Text);
fb.FeedbackLevel = Level;
Add(fb);
return fb;
}
public void Add(Feedback fb, int Progress)
{fb.Progress = Progress;
Add(fb);
}public
ExceptionFeedback Add(
Exception ex)
{
ExceptionFeedback ef = new ExceptionFeedback(ex);
Add(ef);
return ef;
}#endregion
#region Zero result
///
<summary>
/// By default this value is <c>false</c>, but the executing (batch) code
/// can set this value to indicate that all in all nothing has happened.
/// (eg, code to import file did not find a file)
/// To include a reason, use <see cref="SetZeroResult"/>
/// </summary>
public bool ZeroResult
{
get {
return lastzeroresult !=
null; }
set{
if (ZeroResult !=
value)
{
if (
value)
SetZeroResult("No reason specified");
else
LastZeroResult = null;
} } }ZeroResultFeedback lastzeroresult;
internal
ZeroResultFeedback LastZeroResult
{
get
{
return lastzeroresult;
}
set
{if (lastzeroresult ==
value)
return;
/*
if (Parent != null)
{if (value != null)
Parent.LastZeroResult = value;
else if (Parent.lastzeroresult == lastzeroresult)
Parent.LastZeroResult = null;
}*/lastzeroresult =
value;
} }public
class ZeroResultFeedback :
Feedback{
public
readonly string Reason;
public ZeroResultFeedback(
string reason)
: base("Batch has zero result: [" + reason + "]")
{
this.FeedbackLevel = FeedBackLevel.Important;
this.Reason = reason;
} }
///
<summary>
/// The reason that was indicated that the code where this feedback
/// was used with did not actually do anything.
/// This value can be set when using <see cref="SetZeroResult"/>
/// </summary>
public string ZeroResultReason
{
get
{
if (ZeroResult) return lastzeroresult.Reason;
return null;
} }
public void SetZeroResult(string reason)
{ZeroResultFeedback fb = new ZeroResultFeedback(reason);
Add(fb);
LastZeroResult = fb;
}
#endregion
public
Feedback Last
{
get
{
return this[Count - 1];
} }public
DateTime StartTime
{
get
{
if (Count == 0) return DateTime.MinValue;
return this[0].DateTime;
} }public
DateTime EndTime
{
get
{
if (Count == 0) return DateTime.MinValue;
return Last.LastUpdate;
} }public
TimeSpan Duration
{
get { return EndTime.Subtract(StartTime); }
}private int maxcount;
///
<summary>
/// The maximum amount of entries allowed
/// </summary>
public int MaximumValues
{
get {
return maxcount; }
set{
maxcount = value;
CheckMaxValues();
}
}void CheckMaxValues()
{
if (maxcount > 0)
{
while (Count > maxcount)
RemoveAt(0);
} }#region Save/Mail
///
<summary>
/// Description is used when saving or mailing the list, so
/// this should contain information about the contents
/// </summary>
public string Description =
"Feedback list"
public string Save()
{
string file = Application.CommonAppDataPath + @"\Feedback\"
if (!Directory.Exists(file)) Directory.CreateDirectory(file);
string notallowed = " /\\", descr = Description;
if (descr !=
null)
for (
int i = 0; i < notallowed.Length; i++)
{
descr = descr.Replace(notallowed[i], '_');
}
elsedescr = "feedback"
file += descr +
".htm"
Save(file);
return file;
}
public void Save(
string file)
{
lock (
this)
{
StreamWriter sw = new StreamWriter(file, true, System.Text.Encoding.ASCII);
sw.Write(GetTotalFeedBackText());
sw.Close();
Add("List saved to " + file);
} }
public
void Mail()
{
Mail(
(ZeroResult
? "Zero result "
: null)
+ "Batch Feedback '" + Description + "'");
}
public
void Mail(
string Subject)
{
Feedback.DefaultSendMailMethod(Subject,GetTotalFeedBackText(false),true);
}public
void MailExceptions()
{
Feedback.DefaultSendMailMethod(
"Errors in " + Description,
AppendFeedbackText(
new AppendInfoSettings(),EnumerateAll<ExceptionFeedback>(true)).ToString(),
true
);
}public
string GetTotalFeedBackText()
{
return GetTotalFeedBackText(true);
}
public string GetTotalFeedBackText(bool AllowScripts)
{return AppendFeedbackText(new AppendInfoSettings { AllowScripts=AllowScripts}).ToString();
}        
public
bool HasExceptions()
{
foreach (
Feedback fb
in this)
{
if (fb.FeedbackLevel==
FeedBackLevel.Exception)
return true;
if (fb.HasChildren && fb.Children.HasExceptions())
return true;
}
return false; }public
IEnumerable<
Feedback> EnumerateAll<FeedbackType>()
where FeedbackType : Feedback
{return EnumerateAll<FeedbackType>(true);
}
public IEnumerable<Feedback> EnumerateAll<FeedbackType>(bool IncludeChildren)where FeedbackType : Feedback
{foreach (
Feedback fb
in this)
{
if (fb
is FeedbackType)
yield return fb;
if (IncludeChildren && fb.HasChildren)
{
foreach (FeedbackType f
in fb.Children.EnumerateAll<FeedbackType>(
true))
{
yield return f;
} } } }public
StringBuilder AppendFeedbackText(
StringBuilder sb,
bool AllowScripts)
{
return AppendFeedbackText(new AppendInfoSettings(sb) { AllowScripts = AllowScripts });
}
public StringBuilder AppendFeedbackText(AppendInfoSettings info)
{return AppendFeedbackText(info, this);
}
public StringBuilder AppendFeedbackText(AppendInfoSettings info, IEnumerable<Feedback> list)
{var sb = info.StringBuilder;
sb.Append(
"<DIV style=\"border='1px black solid'\">");
if (Parent ==
null)
sb.Append("\tFeedback ")
.Append(Description)
.Append(". Info created on ").Append(DateTime.Now);
if (ZeroResult)
sb.Append(
"<div style=\"border:'1 solid black';background-color:cyan\"><b>Finished with zero result: </b>")
.Append(ZeroResultReason)
.Append("</div>");
sb.Append(
"<HR>");
int i = 0;
foreach (
Feedback fb
in list)
{
fb.AppendInfo(info, i++);
if (info.MaxLines > 0 && i == info.MaxLines) break;
}
sb.Append(
"</DIV>");
return sb;
}
public void ShowHTML()
{
if (System.Threading.
Thread.CurrentThread.GetApartmentState() == System.Threading.
ApartmentState.STA)
{
Form f = new Form();
WebBrowser wb = new WebBrowser();
wb.Dock = DockStyle.Fill;
f.Controls.Add(wb);
wb.DocumentText = GetTotalFeedBackText();
f.WindowState = FormWindowState.Maximized;
f.Show();
}
else//Unfortunately, using the webbrowser requires an STA
//if this is not the case of the current thread, save and show with explorer
System.Diagnostics.Process.Start(Save());
}
#endregion
///
<summary>
/// Creates a <see cref="FeedbackForm"/> with a <see cref="FeedbackControl"/> and shows it or
/// shows an existing form if it was already created
/// </summary>
public FeedbackForm Show()
{
return Show((IWin32Window)null);
}
public FeedbackForm Show(IWin32Window Owner)
{if (Owner
is Control)
return Show(Owner as Control);
if (frm ==
null)
{
CreateFeedbackForm();
frm.Show(Owner);
}
elsefrm.Activate();
return frm;
}
delegate FeedbackForm show(Control owner);
public
FeedbackForm Show(
Control Owner)
{
if (Owner ==
null)
return Show();
if (Owner.InvokeRequired)
return (
FeedbackForm)
Owner.Invoke(new show(Show), Owner);
if (frm ==
null)
{
CreateFeedbackForm(Owner);
Form f = Owner.FindForm();
if (f !=
null)
{
if (f.IsMdiContainer)
frm.MdiParent = f;
else
if (f.IsMdiChild)
frm.MdiParent = f.MdiParent;
}if (frm.MdiParent ==
null)
frm.Show(Owner);
else
frm.Show();
}
elsefrm.Activate();
return frm;
}
FeedbackForm frm;
public
void ShowDialog()
{
ShowDialog(null);
}
public
void ShowDialog(
IWin32Window Owner)
{
CreateFeedbackForm().ShowDialog(Owner);
}
///
<summary>
/// creates a feedbackform based on this collection
/// </summary>
/// <returns></returns>
public FeedbackForm CreateFeedbackForm()
{
createForm();
return frm;
}
void createForm()
{
if (frm ==
null)
frm = new FeedbackForm(this);
}
/// <summary>
/// This overload of <see cref="CreateFeedbackForm()"/> makes sure
/// the <see cref="FeedbackForm"/> is created on the same thread
/// as the <c>ThreadControl</c>
/// </summary>
public FeedbackForm CreateFeedbackForm(Control ThreadControl)
{if (ThreadControl ==
null || !ThreadControl.InvokeRequired)
return CreateFeedbackForm();
CloseFeedbackForm();
ThreadControl.Invoke(new System.Threading.ThreadStart(createForm));
return frm;
}
///
<summary>
/// If a <see cref="FeedbackForm"/> is shown, this method will close it
/// </summary>
public void CloseFeedbackForm()
{
if (frm !=
null)
{
try
{
if (frm.InvokeRequired)
frm.Invoke(new System.Threading.ThreadStart(frm.Dispose));
else
frm.Dispose();
}
catch { }
frm = null; } }
/*internal void FormClosed()
{
frm = null;
}
* */}
/// <summary>
/// Basically just a form with a <see cref="FeedbackControl"/>
/// </summary>
public class FeedbackForm :
Form{
FeedbackControl fc =
new FeedbackControl();        
public FeedbackForm()
{
Text = DefaultText;      
KeyPreview = true;            
fc.CollectionChanged += new EventHandler(fc_CollectionChanged);
fc.Dock = DockStyle.Fill;
Controls.Add(fc);
}
public FeedbackForm(
FeedbackCollection collection)
: this()
{
this.Collection = collection;
}public
Feedback Add(
Feedback fb)
{
return fc.Add(fb);
}public
FeedbackCollection Collection
{
get { return fc.Collection; }
set { fc.Collection = value; }
}
void fc_CollectionChanged(
object sender,
EventArgs e)
{
SetText();
}
const
string DefaultText =
"Feedback information"
void SetText()
{
string text = DefaultText;
if(Collection!=
null && Collection.Description!=
null)
text = "[" + Collection.Description + "] " + text;
Text = text;
}public
static Size DefaultFormSize =
new Size(300, 300);
protected override Size DefaultSize
{
get
{
return DefaultFormSize;
} }protected
override void OnKeyDown(
KeyEventArgs e)
{
if (e.KeyCode == Keys.Escape) Close();
base.OnKeyDown(e);
}protected
override void OnClosed(
EventArgs e)
{
Collection.CloseFeedbackForm();
}
}
///
<summary>
/// A control containing a <see cref="FeedbackListBox"/> and
/// some extra linklabels for extra gui options
/// </summary>
public class FeedbackControl :
Control{
FeedbackListBox lb =
new FeedbackListBox();
BottomPanel pnlBottom;
public FeedbackControl()
{
lb.Dock = DockStyle.Fill;
lb.CollectionChanged += new EventHandler(lb_DataSourceChanged);
Controls.Add(lb);
pnlBottom = new BottomPanel();
Controls.Add(pnlBottom);
}
void lb_DataSourceChanged(
object sender,
EventArgs e)
{
pnlBottom.Visible = Collection != null;
}
///
<summary>
/// By Asigning a collection, the listbox will update
/// itself when messages are received.
/// NB: each collection can have only one FeedbackControl asigned to it.
/// </summary>[
DefaultValue(
null)]
public FeedbackCollection Collection
{
get {
return lb.Collection; }
set{
lb.Collection = value;
}
}public
event EventHandler CollectionChanged
{
add { lb.CollectionChanged += value; }
remove { lb.CollectionChanged -= value; }
}class
BottomPanel :
Panel{
LinkLabel
llMail = new LinkLabel(),
llSave = new LinkLabel(),
llShow = new LinkLabel();
public BottomPanel()
{
Dock = DockStyle.Bottom;
Padding = new Padding(1);
Height = 20;
AddLabel(llMail);
AddLabel(llSave);
AddLabel(llShow);
llMail.Click += new EventHandler(llMail_Click);
llMail.Text = "Mail list"
llSave.Click += new EventHandler(llSave_Click);
llSave.Text = "Save list"
llShow.Click += new EventHandler(llShow_Click);
llShow.Text = "Show overview"
}
void llShow_Click(
object sender,
EventArgs e)
{
if (InvokeRequired)
Invoke(new EventHandler(llShow_Click), sender, e);
else
coll.ShowHTML();
}new
FeedbackControl Parent
{
get { return base.Parent as FeedbackControl; }
}
FeedbackCollection coll
{get { return Parent.Collection; }
}
void llSave_Click(object sender, EventArgs e)
{if (InvokeRequired)
Invoke(new EventHandler(llSave_Click), sender, e);
else
try
{
SaveFileDialog sf = new SaveFileDialog();
sf.Filter = "HTML file|*.htm;*.html"
if (sf.ShowDialog(
this) ==
DialogResult.OK)
{
coll.Save(sf.FileName);
}
}
catch (
Exception ex)
{
MessageBox.Show("Error saving list:\r\n" + ex.Message);
}}
void llMail_Click(
object sender,
EventArgs e)
{
if (InvokeRequired)
Invoke(new EventHandler(llMail_Click), sender, e);
else
try
{
coll.Mail();
}
catch (
Exception ex)
{
MessageBox.Show("Error mailing list:\r\n" + ex.Message);
} }void AddLabel(
LinkLabel ll)
{
ll.Dock = DockStyle.Left;
Controls.Add(ll);
}
}
public
Feedback Add(
Feedback fb)
{
return lb.Add(fb);
}public
int Count
{
get { return lb.Count; }
}public
Feedback this[
int Index]
{
get { return lb[Index]; }
}public
class FeedbackAlivePanel :
Panel{
PulseRectangle pr =
new PulseRectangle();
public FeedbackAlivePanel()
{
SetStyle(ControlStyles.UserPaint
| ControlStyles.AllPaintingInWmPaint, true);
}
protected
override void OnResize(
EventArgs eventargs)
{
pr.Bounds = Bounds;
Increase();
}
public
void Increase()
{
Invalidate();
}
protected
override void OnPaint(
PaintEventArgs e)
{
pr.Paint(e.Graphics);
}
}
}
///
<summary>
/// A listbox specifically for feedback items. The items are graphically shown to clearly
/// outline their type of feedback
/// </summary>
public class FeedbackListBox :
ListBox{
public FeedbackListBox()
{
DrawMode =
DrawMode.OwnerDrawFixed;
SetStyle(
ControlStyles.AllPaintingInWmPaint
| ControlStyles.OptimizedDoubleBuffer, true);
items =
new ItemCollection(
this);
}
public
Feedback this[
int index]
{
get
{
if (coll ==
null)
throw new Exception("No collection has been started yet");
return coll[index];
} }[
Browsable(
false)]
[
DesignerSerializationVisibility(
DesignerSerializationVisibility .Hidden)]
public new ItemCollection Items
{
get { return items; }
}ItemCollection items;
public class ItemCollection{
public
readonly FeedbackListBox Owner;
public ItemCollection(
FeedbackListBox Owner)
{
this.Owner = Owner;
}public
Feedback this[
int Index]
{
get
{
return Owner[Index];
} }public int Count { get { return Owner.Count; } }
public
void Add(
Feedback fb)
{
Owner.Add(fb);
}
public
int IndexOf(
Feedback fb)
{
return Owner.IndexOf(fb);
}}
public
Feedback Add(
Feedback fb)
{
if (coll == null) Collection = new FeedbackCollection();
coll.Add(fb);
return fb;
}
void Collection_ListChanged(
object sender,
ListChangedEventArgs e)
{
switch (e.ListChangedType)
{
case
ListChangedType.ItemAdded:
case ListChangedType.ItemChanged:
case ListChangedType.ItemDeleted:
//this inbetween class is used because in threading environments
//the collection can be updated before the invoke is finishedupdate(
new updateinfo{
Index=e.NewIndex,
Item=((FeedbackCollection)sender)[e.NewIndex],
Type= e.ListChangedType
});
break;
}}
class
updateinfo{            
public int Index;
public Feedback Item;
public ListChangedType Type;
}Queue<
updateinfo> updates =
new Queue<
updateinfo>();
volatile bool updating;
void update(
updateinfo info)
{
lock (updates)
updates.Enqueue(info);
try
{
if (!updating && !IsDisposed)
BeginInvoke((
Void)
delegate()
{
update();
});
}
catch (Exception ex)
{Console.WriteLine(ex.Message);
}}
void update()
{
if (updating)
return;
updating =
true;
updateinfo info ;
while(updates.Count>0)
{
lock(updates)
info = updates.Dequeue();
switch (info.Type)
{
case
ListChangedType.ItemAdded:
Init(info.Item, info.Index);
break;
case
ListChangedType.ItemChanged:
Update(info.Item, info.Index);
break;
case
ListChangedType.ItemDeleted:
Remove(info);
break;
} }
Application.DoEvents();
updating = false; }
void Init(
Feedback fb,
int index)
{
fb.Updated += new EventHandler(Feedback_Updated);                        
base.Items.Insert(index, fb);
SetIndex(index);
}
void Remove(
updateinfo inf)
{
inf.Item.Updated -= new EventHandler(Feedback_Updated);
base.Items.RemoveAt(inf.Index);
}
void Feedback_Updated(
object sender,
EventArgs e)
{
var u = new updateinfo();
u.Item= (Feedback)sender;
u.Index=IndexOf(u.Item);
u.Type =u.Index==-1 ? ListChangedType.ItemDeleted : ListChangedType.ItemChanged;
update(u);
}delegate void Void();
void UpdateSummary()
{
}
void Update(Feedback fb, int index)
{
if (fb.IsAliveIndicator)
UpdatePulse(fb);
//System.Diagnostics.Debug.Assert(index >= 0);
if (index >= 0)
{
Rectangle r = GetRectangle(index);
Invalidate(r);
SetIndex(index);
Application.DoEvents();
}
elsefb.Updated -= new EventHandler(Feedback_Updated);
}
Rectangle GetRectangle(
int index)
{
Rectangle r = ClientRectangle;
r.Height = this.ItemHeight;
r.Y = r.Height*index;
return r;
}
void SetIndex(int index)
{SelectedIndex = index;
}public
int IndexOf(
Feedback fb)
{
if (coll == null) return -1;
return coll.IndexOf(fb);
}
public int Count
{get
{
if (coll == null) return 0;
return coll.Count;                
} }protected
override void OnDrawItem(
DrawItemEventArgs e)
{
object item = base.Items[e.Index];
Feedback fb = (Feedback)item ;
FeedbackItemInfo inf = new FeedbackItemInfo(fb,e,coll.StartTime,this);
fb.GetPainter(inf).Paint();
}
public event EventHandler CollectionChanged;
FeedbackCollection coll;
[
DefaultValue(
null)]
public FeedbackCollection Collection
{
get
{
return coll;
}
set
{if (coll ==
value)
return;
BeginUpdate();
base.Items.Clear();
if(coll!=
null)
coll.ListChanged += new ListChangedEventHandler(Collection_ListChanged);
//base.DataSource =  DataSource not used because of threading problems
coll =
value;                
if (coll !=
null)
{                    
coll.ListChanged +=
new ListChangedEventHandler(Collection_ListChanged);
for (
int i = 0; i < coll.Count; i++)
{
Init(coll[i], i);
}                    
}
EndUpdate();
if (CollectionChanged !=
null)
CollectionChanged(this, EventArgs.Empty);
} }
[DesignerSerializationVisibility(DesignerSerializationVisibility .Hidden)]
public new FeedbackCollection DataSource
{get { return coll; }
}Dictionary<
Feedback,
PulseRectangle> pulses;
void UpdatePulse(
Feedback fb)
{
GetPulse(fb).Update();
}
public
PulseRectangle GetPulse(
Feedback fb)
{
if (pulses ==
null)
pulses = new Dictionary<Feedback, PulseRectangle>();
PulseRectangle pr;
if (!pulses.TryGetValue(fb,
out pr))
{
pr = new PulseRectangle();
pr.Count = 20;
pulses.Add(fb, pr);
}
return pr;
}protected
override void OnClick(
EventArgs e)
{
if (mouseonbutton !=
null)
{
mouseonbutton.ShowExtraInfo(this);
}
base.OnClick(e);
}Feedback mouseonbutton;
protected override void OnMouseMove(
MouseEventArgs e)
{
Feedback onbutton =
null;
if (Count > 0 &&
FeedbackItemPainter.ExtraInfoButtonArea.Contains(e.X,e.Y))
{
int index = IndexFromPoint(e.Location);
if (index != -1 && index < Count)
onbutton = this[index];
}if (mouseonbutton != onbutton)
{
if (onbutton !=
null && onbutton.CanShowExtraInfo)
mouseonbutton = onbutton;
else
mouseonbutton = null;
Cursor = mouseonbutton != null ? Cursors.Hand : null;
}
}
}
public
class FeedbackItemInfo{        
public readonly DrawItemEventArgs DrawItemEventArgs;
public readonly bool IsSelected;
public readonly Feedback Item;
public readonly int Index;
public readonly Rectangle Bounds;
public readonly DateTime CollectionStartTime;
public readonly FeedbackListBox Owner;
public FeedbackItemInfo(
Feedback Item,
DrawItemEventArgs e,
DateTime CollectionStartTime,
FeedbackListBox Owner)
{
this.Item = Item;
this.DrawItemEventArgs = e;            
this.IsSelected = (e.State & DrawItemState.Selected) > 0;
this.Index = e.Index;            
this.Owner = Owner;
this.Bounds = e.Bounds;
this.CollectionStartTime = CollectionStartTime;
}}
public
class FeedbackItemPainter{
Rectangle r;
public FontFamily FontFamily;
public readonly Rectangle MessageArea;
public readonly FeedbackItemInfo Info;
public readonly Graphics Graphics;
public FeedbackItemPainter(
FeedbackItemInfo info)
{
this.Info = info;
this.r = info.Bounds;
this.FontFamily = info.DrawItemEventArgs.Font.FontFamily;
MessageArea = new Rectangle(MessageOffset, r.Y, r.Width - MessageOffset, r.Height);
this.Graphics = info.DrawItemEventArgs.Graphics;
}public
const intExtraInfoOffset = 75,
ExtraInfoWidth = 10,
ProgressWidth = 40,
MessageOffset = 90,
TimeOffset = 32;
public static readonly Rectangle ExtraInfoButtonArea = new Rectangle(ExtraInfoOffset, 0, ExtraInfoWidth, int.MaxValue);
public
void DrawDateTime()
{
DrawDateTime(Info.Index > 0,Brushes.Black);
}
public virtual void DrawDateTime(
bool Relative,
Brush b)
{
//clear background
ClearDateTimeBackGround(
Brushes.White);
//draw text
if (Relative)
DrawRelativeTime(Info.Item.LastUpdate.Subtract(Info.CollectionStartTime),b);
else
DrawAbsoluteTime(b);
}void DrawAbsoluteTime(
Brush b)
{
Graphics.DrawString(
Info.Item.DateTime.ToShortDateString() + " " + Info.Item.DateTime.ToString("hh:mm.ss"),
GetFont(6.5f), b, r.Left, r.Top + 1);
}
void DrawRelativeTime(
TimeSpan diff,
Brush b)
{
Graphics.DrawString(
string.Format(
"+{0:00}:{1:00}.{2:00}.{3:000}",
diff.Hours, diff.Minutes, diff.Seconds, diff.Milliseconds),
GetFont(7.2f)
, b, r.Left, r.Top);
}
public
Font GetFont(
float size)
{
return new Font(FontFamily, size);
}public
virtual void ClearDateTimeBackGround(
Brush b)
{
Graphics.FillRectangle(b, DateTimeArea);
}
public
Rectangle DateTimeArea
{
get
{
return new Rectangle(r.Left, r.Top, r.Left + ExtraInfoOffset, r.Height);
} }public
virtual Color GetBackgroundColor()
{
if(Info.IsSelected)
return Color.Blue;
return Color.White;
}public
void DrawSelectionRectangle()
{
DrawSelectionRectangle(GetBackgroundColor());
}
public virtual void DrawSelectionRectangle(
Color c)
{
Graphics.DrawRectangle(new Pen(c), r);
ClearMessageBackGround(c);
}
public void ClearMessageBackGround(
Color c)
{
Graphics.FillRectangle(new SolidBrush(c), MessageArea);
}
public
void DrawExtraInfoButton()
{
Rectangle rectEI = ExtraInfoButtonArea;
rectEI.Y = r.Y;
rectEI.Height = r.Height;
DrawExtraInfoButton(rectEI);
}public
virtual void DrawExtraInfoButton(
Rectangle r)
{
ControlPaint.DrawButton(Graphics, r, ButtonState.Normal);
r.X += 2;
Graphics.DrawString("i", GetFont(7), Brushes.Blue, r);
}public
void DrawMessage()
{
DrawMessage(offset);
}
public
void DrawMessage(
int Offset)
{
Color c = Info.IsSelected ? Color.WhiteSmoke : Color.Black;            
Rectangle r = MessageArea;
r.X += Offset;
r.Width -= Offset;
DrawMessage(r, new SolidBrush(c));
}
public virtual void DrawMessage(Rectangle r,Brush b)
{            Font f = GetFont(7);
if (Info.Item.Level > 0)
f = new Font(f, FontStyle.Bold);
Graphics.DrawString(Info.Item.Message, f, b, r);
}
int offset;
public
virtual void PaintProgress()
{
RectangleF rectProg = GetProgressRectangle();
rectProg.X++;
rectProg.Inflate(0, -1);
Graphics.FillRectangle(
Brushes.White, rectProg);
Pen p =
Pens.Black;
Graphics.DrawRectangle(p,
Rectangle.Round(rectProg));
float penwidth = .5f;
rectProg.Inflate(-penwidth, -penwidth);
rectProg.Width *= Info.Item.ProgressPercentage;
Graphics.FillRectangle(
Brushes.Green, rectProg);
Font f = GetFont(7);
Graphics.DrawString(Info.Item.GetProgressString(), f,
Brushes.Navy, rectProg.Left + 1, rectProg.Top - 1);
}public
virtual void PaintAlive()
{
Rectangle r = GetProgressRectangle();
r.Inflate(0, -1);
PulseRectangle pr = Info.Owner.GetPulse(Info.Item);
pr.Bounds = r;
pr.Paint(Graphics);
}protected
Rectangle GetProgressRectangle()
{
Rectangle r = MessageArea;
r.X += offset;
offset += ProgressWidth + 2;
r.Width = ProgressWidth;
return r;
}protected
void IncreaseOffset(
int by)
{
offset += by;
}
public virtual void Paint()
{
DrawDateTime();
if (Info.Item.CanShowExtraInfo)
{
DrawExtraInfoButton();
}
DrawSelectionRectangle();
//draw progress
if (Info.Item.Progress > 0)
{
PaintProgress();
}
//draw alive
if (Info.Item.IsAliveIndicator)
{
PaintAlive();
}
//draw message
DrawMessage();
}
}
public
class PulseRectangle{
private Rectangle bounds;
public
Rectangle Bounds
{
get {
return bounds; }
set{
if (bounds ==
value)
return;
Rectangle r = bounds;
bounds =
value;
if (r.Width !=
value.Width || ps ==
null)
Update();
else
FillDrawPoints();
} }void transform(
int x,
int y)
{
if (x == 0 && y == 0)
return;
for (
int i = 0; i < ps.Length; i++)
{
ps[i].X -= x;
ps[i].Y -= y;
}
}/// <summary>
/// The number of points to draw.
/// Call Update manually after setting this value
/// </summary>
public int Count = 200;
private
Point[] ps, drawpoints;
public void Update()
{
Random r =
new Random();
double step = (
double)bounds.Width / Count;
ps =
new Point[Count];
drawpoints =
null;
for (
int i = 0; i < Count; i++)
ps[i] = new Point((int)(step * i), r.Next(100));
FillDrawPoints();
}
/// <summary>
/// The pen with which the pulses are drawn
/// </summary>
public Pen Pen = Pens.LightGreen;public Brush BackGroundBrush = Brushes.Black;
void FillDrawPoints()
{
int left = bounds.Left, h = bounds.Height, top = bounds.Top;
drawpoints =
new Point[ps.Length];
for (
int i = 0; i < ps.Length; i++)
{
drawpoints[i].X = ps[i].X + left;
drawpoints[i].Y = top + (int)(h * ps[i].Y / 100);
}
}
public void Paint(Graphics g)
{if (ps == null) Update();
g.FillRectangle(BackGroundBrush, bounds);
g.DrawLines(Pen, drawpoints);
}
}
///
<summary>
/// The importance of the feedback
/// </summary>
public enum FeedBackLevel{
Custom = 100,
Trivial = -100,
Normal = 0,
SuperNormal = 200,
Exception = 500,
Important = 1000
}
public
interface IFeedBackSupporter{
FeedbackCollection Feedback { get; }
}#region Specific feedback
public class FileReferenceFeedback :
Feedback{
public
readonly string File;
public FileReferenceFeedback(
string MessagePrefix,
string File)
: base(
MessagePrefix
+ System.IO.Path.GetFileName(File)
+ " in "
+ System.IO.Path.GetDirectoryName(File))
{
this.File = File;
}
public FileReferenceFeedback(string File): this("File: ", File)
{}
public
override bool CanShowExtraInfo
{
}public
override void ShowExtraInfo(
IWin32Window Owner)
{
System.Diagnostics.Process.Start("Explorer", "/select," + File);
}
}
public
class ExceptionFeedback :
Feedback{
public
readonly Exception Exception;
public ExceptionFeedback(
Exception ex)
{
Exception = ex;
FeedbackLevel = FeedBackLevel.Exception;
System.Diagnostics.StackTrace stack = new System.Diagnostics.StackTrace(ex);
Message = "Error '" + ex.Message + "' occured on " + stack.GetFrame(0);
}
public
override bool CanShowExtraInfo
{
}           public
override void ShowExtraInfo(
IWin32Window Owner)
{
DefaultShowExceptionMethod(Exception);            
}
class
ErrorPainter :
FeedbackItemPainter{
public ErrorPainter(
FeedbackItemInfo inf)
: base(inf)
{
}
public
override void DrawMessage(
Rectangle r,
Brush b)
{
Rectangle ri = r;
ri.Width = ri.Height;
Graphics.DrawIcon(SystemIcons.Error, ri);
r.X = ri.Right;
base.DrawMessage(r,Info.IsSelected ? Brushes.Orange : Brushes.Red);
}}
public
override FeedbackItemPainter GetPainter(
FeedbackItemInfo info)
{
return new ErrorPainter(info);
}protected
override void AppendExtraInfo(
AppendInfoSettings info)
{
new ExceptionInfo(Exception).AppendMessage(info.StringBuilder);
}}
#endregion
}
. . .
Also needed: (this is used to show complete exception information in HTML, you could also strip the exception adding functionality, but I use this class a lot to quickly debug and get better stacktraces than the default .net block of code.