// --------------------------------------------------------------------------------------------------------------------
// <copyright file="TraceOutputControl.xaml.cs" company="Catel development team">
// Copyright (c) 2008 - 2011 Catel development team. All rights reserved.
// </copyright>
// <summary>
// Interaction logic for TraceOutputControl.xaml
// </summary>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Catel.Diagnostics;
using Catel.Windows.Input;
namespace Catel.Windows.Controls
{
/// <summary>
/// Interaction logic for TraceOutputControl.xaml
/// </summary>
public partial class TraceOutputControl : UserControl
{
#region Variables
#endregion
#region Constructor & destructor
/// <summary>
/// Initializes a new instance of the <see cref="TraceOutputControl"/> class.
/// </summary>
public TraceOutputControl()
{
InitializeComponent();
TraceEntryCollection = new ObservableCollection<TraceEntry>();
FilteredTraceEntryCollection = new FilteredTraceList(TraceEntryCollection);
Resources["FilteredTraceEntryCollection"] = FilteredTraceEntryCollection;
TraceListener = new OutputTraceListener();
Trace.Listeners.Add(TraceListener);
TraceListener.ActiveTraceLevel = TraceLevel.Verbose;
TraceListener.WrittenLine += traceListener_WrittenLine;
CommandBindings.Add(new CommandBinding(WindowCommands.Clear, Clear_Executed, Clear_CanExecute));
CommandBindings.Add(new CommandBinding(WindowCommands.CopyToClipboard, CopyToClipboard_Executed, CopyToClipboard_CanExecute));
}
#endregion
#region Delegates
/// <summary>
/// Delegate that can be used to re-invoke a method when it comes from a different thread.
/// </summary>
private delegate void ClearDelegate();
/// <summary>
/// Delegate that can be used to re-invoke a method when it comes from a different thread.
/// </summary>
/// <param name="message">Message to write</param>
/// <param name="eventType">Trace event type</param>
private delegate void WriteDelegate(string message, TraceEventType eventType);
#endregion
#region Properties
/// <summary>
/// Gets or sets the trace entry collection.
/// </summary>
/// <value>The trace entry collection.</value>
private ObservableCollection<TraceEntry> TraceEntryCollection { get; set; }
/// <summary>
/// Gets or sets the filtered trace entry collection.
/// </summary>
/// <value>The filtered trace entry collection.</value>
private FilteredTraceList FilteredTraceEntryCollection { get; set; }
/// <summary>
/// Gets or sets the trace listener.
/// </summary>
/// <value>The trace listener.</value>
private OutputTraceListener TraceListener { get; set; }
/// <summary>
/// Gets or sets TraceLevelOutput.
/// </summary>
/// <remarks>
/// Wrapper for the TraceLevelOutput dependency property.
/// </remarks>
public TraceLevel TraceLevelOutput
{
get { return (TraceLevel)GetValue(TraceLevelOutputProperty); }
set { SetValue(TraceLevelOutputProperty, value); }
}
/// <summary>
/// DependencyProperty definition as the backing store for TraceLevelOutput.
/// </summary>
public static readonly DependencyProperty TraceLevelOutputProperty = DependencyProperty.Register("TraceLevelOutput", typeof(TraceLevel),
typeof(TraceOutputControl), new UIPropertyMetadata(TraceLevel.Verbose, TraceLevelOutput_Changed));
/// <summary>
/// Returns the available trace levels.
/// </summary>
public IEnumerable<TraceLevel> AvailableTraceLevelCollection
{
get
{
foreach (TraceLevel level in Enum.GetValues(typeof(TraceLevel)))
{
yield return level;
if (TraceListener.ActiveTraceLevel == level)
{
break;
}
}
}
}
#endregion
#region Command bindings
/// <summary>
/// Determines whether the user can execute the Clear command.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">The event data.</param>
private void Clear_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
/// <summary>
/// Handled when the user invokes the Clear command.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event Arguments.</param>
private void Clear_Executed(object sender, ExecutedRoutedEventArgs e)
{
ClearData();
}
/// <summary>
/// Determines whether the user can execute the CopyToClipboard command.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">The event data.</param>
private void CopyToClipboard_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
if (logListView.SelectedItems.Count == 0)
{
return;
}
e.CanExecute = true;
}
/// <summary>
/// Handled when the user invokes the CopyToClipboard command.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">Event Arguments.</param>
private void CopyToClipboard_Executed(object sender, ExecutedRoutedEventArgs e)
{
ListViewItem sourceItem = e.OriginalSource as ListViewItem;
string text = string.Empty;
if (sourceItem != null)
{
text = TraceEntriesToString(logListView.SelectedItems.Cast<TraceEntry>());
}
if (!string.IsNullOrEmpty(text))
{
Clipboard.SetText(text, TextDataFormat.Text);
}
}
#endregion
#region Methods
/// <summary>
/// Invoked when the TraceLevelOutput dependency property has changed.
/// </summary>
/// <param name="sender">The object that contains the dependency property.</param>
/// <param name="e">The event data.</param>
private static void TraceLevelOutput_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
TraceOutputControl typedSender = sender as TraceOutputControl;
if (typedSender != null)
{
typedSender.OnTraceLevelOutputPropertyChanged(e);
}
}
/// <summary>
/// Invoked when the WrittenLine event of the TraceListener is raised.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="eventType">Type of the event.</param>
private void traceListener_WrittenLine(string message, TraceEventType eventType)
{
WriteLine(message, eventType);
}
/// <summary>
/// Raises the <see cref="E:TraceLevelOutputPropertyChanged"/> event.
/// </summary>
/// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
private void OnTraceLevelOutputPropertyChanged(DependencyPropertyChangedEventArgs e)
{
FilteredTraceEntryCollection.LevelToShow = (TraceLevel)e.NewValue;
// Don't scroll if we have no listview or if the collection is empty
if (FilteredTraceEntryCollection.Count == 0)
{
return;
}
ScrollToBottom();
}
/// <summary>
/// Clears the output window.
/// </summary>
public void ClearData()
{
if (!Dispatcher.CheckAccess())
{
Dispatcher.Invoke(new ClearDelegate(ClearData), new object[] { });
return;
}
TraceEntryCollection.Clear();
}
/// <summary>
/// Writes a line to the output window.
/// </summary>
/// <param name="message">Message to write.</param>
/// <param name="eventType">Type of the event.</param>
public void WriteLine(string message, TraceEventType eventType)
{
if (!Dispatcher.CheckAccess())
{
Dispatcher.BeginInvoke(new WriteDelegate(WriteLine), new object[] { message, eventType });
return;
}
TraceEntryCollection.Add(new TraceEntry(TraceHelper.ConvertTraceEventTypeToTraceLevel(eventType), message));
ScrollToBottom();
}
/// <summary>
/// Moves the cursor down so the latest output is visible.
/// </summary>
private void ScrollToBottom()
{
bool scroll = false;
if ((FilteredTraceEntryCollection.Count > 0) && (logListView.SelectedItems.Count == 0))
{
scroll = true;
}
if (scroll)
{
if (logListView.IsVisible)
{
// Get the border of the listview (first child of a listview)
Decorator border = VisualTreeHelper.GetChild(logListView, 0) as Decorator;
// Get scrollviewer
ScrollViewer scrollViewer = border.Child as ScrollViewer;
scrollViewer.ScrollToBottom();
}
else
{
logListView.ScrollIntoView(FilteredTraceEntryCollection.Last());
}
}
}
/// <summary>
/// Converts a list of trace entries to a string.
/// </summary>
/// <param name="entries">The entries.</param>
/// <returns>STring representing the trace entries.</returns>
private string TraceEntriesToString(IEnumerable<TraceEntry> entries)
{
const string columnText = " | ";
int maxTypeLength = AvailableTraceLevelCollection.Max(c => c.ToString("G").Length);
StringBuilder rv = new StringBuilder();
Regex rxMultiline = new Regex(@"(?<=(^|\n)).*", RegexOptions.Multiline | RegexOptions.Compiled);
if (entries != null)
{
foreach (TraceEntry entry in entries)
{
string date = entry.Time.ToString(CultureInfo.CurrentUICulture);
string type = entry.TraceLevel.ToString("G").PadRight(maxTypeLength, ' ');
string datefiller = new String(' ', date.Length);
string typefiller = new String(' ', type.Length);
string message = entry.Message;
MatchCollection matches = rxMultiline.Matches(message);
if (matches.Count > 0)
{
rv.AppendFormat("{0}{4}{1}{4}{2}{3}", date, type, matches[0].Value, Environment.NewLine, columnText);
if (matches.Count > 1)
{
for (int idx = 1, max = matches.Count; idx < max; idx++)
{
rv.AppendFormat("{0}{4}{1}{4}{2}{3}", datefiller, typefiller, matches[idx].Value, Environment.NewLine, columnText);
}
}
}
}
}
return rv.ToString();
}
#endregion
#region Nested classes
/// <summary>
/// Class containing a trace entry as it will be used in the output control.
/// </summary>
public class TraceEntry
{
/// <summary>
/// Initializes a new verbose empty trace entry.
/// </summary>
public TraceEntry()
: this(TraceLevel.Verbose, String.Empty, DateTime.Now) { }
/// <summary>
/// Initializes a new trace entry for the current date/time.
/// </summary>
/// <param name="level"><see cref="TraceLevel"/> of the trace entry.</param>
/// <param name="message">Message of the trace entry.</param>
public TraceEntry(TraceLevel level, String message)
: this(level, message, DateTime.Now) { }
/// <summary>
/// Initializes a new instance that can be fully customized.
/// </summary>
/// <param name="level"><see cref="TraceLevel"/> of the trace entry.</param>
/// <param name="message">Message of the trace entry.</param>
/// <param name="time"><see cref="DateTime"/> when the entry was created.</param>
public TraceEntry(TraceLevel level, String message, DateTime time)
{
Message = message;
TraceLevel = level;
Time = time;
}
/// <summary>
/// Actual trace message.
/// </summary>
public String Message { get; private set; }
/// <summary>
/// Trace level.
/// </summary>
public TraceLevel TraceLevel { get; private set; }
/// <summary>
/// Date/time of the trace message.
/// </summary>
public DateTime Time { get; private set; }
}
/// <summary>
/// Filtered trace list.
/// </summary>
internal class FilteredTraceList : ObservableCollection<TraceEntry>
{
#region Variables
/// <summary>
/// The source with the original data.
/// </summary>
private readonly ObservableCollection<TraceEntry> _source;
/// <summary>
/// The level which needs to be shown. 'Off' means no filtering.
/// </summary>
private TraceLevel _levelToShow;
#endregion
#region Constructor & destructor
/// <summary>
/// Initializes a new instance of the <see cref="FilteredTraceList"/> class.
/// </summary>
/// <param name="source">The source.</param>
public FilteredTraceList(ObservableCollection<TraceEntry> source)
{
_source = source;
_source.CollectionChanged += Source_CollectionChanged;
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the level to show. If set to 'Off' the filter shows all.
/// </summary>
/// <value>The level to show.</value>
public TraceLevel LevelToShow
{
get { return _levelToShow; }
set
{
if (_levelToShow != value)
{
_levelToShow = value;
ResetList();
}
}
}
#endregion
#region Methods
/// <summary>
/// Determines if the given entry matches the filter tracelevel.
/// </summary>
/// <param name="traceEntry">The trace entry.</param>
/// <returns>true if matches of if filter is 'Off', false if not </returns>
private bool ItemMatchesLevel(TraceEntry traceEntry)
{
if (_levelToShow == TraceLevel.Off || _levelToShow == TraceLevel.Verbose)
{
return true;
}
return (traceEntry.TraceLevel <= _levelToShow);
}
/// <summary>
/// Resets the list, copies all matching items from the source to this list.
/// </summary>
private void ResetList()
{
Clear();
if ((_levelToShow == TraceLevel.Off) || (_levelToShow == TraceLevel.Verbose))
{
foreach (TraceEntry item in _source)
{
Add(item);
}
return;
}
var filteredList = from item in _source
where ItemMatchesLevel(item)
select item;
foreach (var item in filteredList)
{
Add(item);
}
}
/// <summary>
/// Handles the CollectionChanged event of the source control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
private void Source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChangedAction action = e.Action;
switch (action)
{
case NotifyCollectionChangedAction.Add:
foreach (var newItem in e.NewItems)
{
TraceEntry traceEntry = newItem as TraceEntry;
if (ItemMatchesLevel(traceEntry)) Add(traceEntry);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (var removedItem in e.OldItems)
{
TraceEntry traceEntry = removedItem as TraceEntry;
if (ItemMatchesLevel(traceEntry)) Remove(traceEntry);
}
break;
case NotifyCollectionChangedAction.Replace:
ResetList();
break;
case NotifyCollectionChangedAction.Move:
ResetList();
break;
case NotifyCollectionChangedAction.Reset:
Clear();
foreach (var item in _source)
{
TraceEntry traceEntry = item;
if (ItemMatchesLevel(traceEntry)) Add(traceEntry);
}
break;
default:
// Shouldn't happen
break;
}
}
#endregion
}
#endregion
}
}