In my previous question I got some pretty good suggestions which really improved the overall performance of the controls, but I decided to extend the application and add some more features :
- You can enable/disable auto-updating.
You can select which processes you want to view currently available options are :
Default - shows all the processes.
Active - shows only active processes.
Foreground - show only foreground processes.
Upon minimizing the application will move to the tray bar (there is small bug there sometimes 2 icons will show, but the second will fade after few seconds.)
You can inspect processes, which shows 3 more additional properties :
Instances running - the amount of active instances of the process.
Process Path - the directory where the .exe is located.
Background Process - Indicates whether a process is background process or not (true/false).
Owner - the user that runs the process DOMAIN / user.
You can also terminate and refresh processes.
Searching options is available with auto suggest from the current shown list of processes.
Note
The application now requires administrator privileges as most processes are hidden if not run as administrator + it prevents most of the exceptions, tho there are few processes that will still deny access.
That pretty much sums up all the new things and this is how the program looks like now :
Let's start inspecting the code than.
BufferedListView
As suggested in the previous question's answer I'm not using a simple class that "extendeds" the ListView
control by setting the DoubleBuffered
property to true by default, to reduce flickering.
public sealed class BufferedListView : ListView
{
public BufferedListView()
{
DoubleBuffered = true;
}
protected override bool DoubleBuffered { get; set; }
}
Extensions.cs
The new extension methods :
RemoveAll(this IDictionary<,>)
with 2 overloads, it works the same way as aList.RemoveAll()
would, but for some reason dictionaries don't have that built-in.SplitOnCapitalLetters(this string)
splits a string on each capital letter.
Class implementation :
public static class Extensions
{
public static string ExtendWithEmptySpaces(this string mainString, int desiredLength)
{
if (mainString.Length >= desiredLength)
{
return mainString;
}
StringBuilder extendedStringBuilder = new StringBuilder(mainString);
for (int i = 0; i < desiredLength - mainString.Length; i++)
{
extendedStringBuilder.Append(" ");
}
return extendedStringBuilder.ToString();
}
public static int GetAverageLengthOf<T>(this IEnumerable<T> collection, Func<T, int> predicate)
=> (int) Math.Ceiling(collection.Average(predicate.Invoke));
public static void RemoveAll<K, V>(this IDictionary<K, V> dict, Func<K, V, bool> match)
{
foreach (var key in dict.Keys.ToArray().Where(key => match(key, dict[key])))
{
dict.Remove(key);
}
}
//this one is slower but prettier.
public static void RemoveAll<K, V>(this IDictionary<K, V> dict, Func<KeyValuePair<K, V>, bool> match)
{
foreach (var key in dict.Keys.ToArray().Where(key => match.Invoke(new KeyValuePair<K, V>(key, dict[key]))))
{
dict.Remove(key);
}
}
public static string SplitOnCapitalLetters(this string inputString)
{
StringBuilder result = new StringBuilder();
foreach (var ch in inputString)
{
if (char.IsUpper(ch) && result.Length > 0)
{
result.Append(' ');
}
result.Append(ch);
}
return result.ToString();
}
}
ProcessInfo.cs
The small wrapper class ProcessInfo
is now moved to a separate file. The content remains unchanged :
internal class ProcessInfo
{
public Process Process { get; }
public TimeSpan TimeActive { get; set; }
public ProcessInfo(Process process, TimeSpan timeActive)
{
Process = process;
TimeActive = timeActive;
}
}
ProcessInspector.cs
I've moved all of the general purpose methods operating on objects of type Process
to a separate dedicated class called ProcessInspector
.
public static class ProcessInspector
{
public static string GetProcessPath(Process process)
{
try
{
string query = "SELECT ExecutablePath FROM Win32_Process WHERE ProcessId = " + process.Id;
var searcher = new ManagementObjectSearcher(query);
var collection = searcher.Get();
return collection.Cast<ManagementObject>().First()["ExecutablePath"].ToString();
}
catch
{
return string.Empty;
}
}
public static string GetProcessOwner(int processId)
{
string query = "SELECT * FROM Win32_Process WHERE ProcessId = " + processId;
var searcher = new ManagementObjectSearcher(query);
var processList = searcher.Get();
var managementObject = processList.Cast<ManagementObject>().First();
string[] argList = { string.Empty, string.Empty };
return Convert.ToInt32(managementObject.InvokeMethod("GetOwner", argList)) == 0
? argList[1] + @"\" + argList[0]
: "NO OWNER";
}
public static int GetActiveProcessId()
{
IntPtr activatedHandle = GetForegroundWindow();
if (activatedHandle == IntPtr.Zero)
{
return -1;
}
int activeProcessId;
GetWindowThreadProcessId(activatedHandle, out activeProcessId);
return activeProcessId;
}
public static bool IsBackgroundProcess(Process p)
{
IntPtr hnd = p.MainWindowHandle;
const uint WS_DISABLED = 0x8000000;
const int GWL_STYLE = -0x10;
bool visible = false;
if (hnd != IntPtr.Zero)
{
int style = GetWindowLong(hnd, GWL_STYLE);
visible = (style & WS_DISABLED) != WS_DISABLED;
}
return !visible;
}
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetWindowThreadProcessId(IntPtr handle, out int processId);
[DllImport("user32.dll")]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
}
Moving on to the last and longest part of the code, the actual form. Now here, we have quite some lines of code (452) and since I don't see a good reason to refactor the class into smaller ones (if you do please point that out in an answer), in order to reduce the size of the file and make it easier to read I've separated the variables from the actual logic in 2 files public partial class MainFormVariables
where all the variables are stored and public partial class MainForm : Form
where everything else is stored.
I've also added a #region for all of the event handlers and turned all of the expression bodied members to return statements so that they can collapse too.
But before I show you the code, I'd like to point out that, thanks to the good suggestions in the previous question's answers the actual update of the controls takes almost no time, but the bottleneck here appears to be the functions that retrieve the process' properties values, which I just can't think of a way to improve as I can't store them since they are constantly changing (right ?) and they are pretty much one liners.. Enough talking here's the code :
MainFormVariables.cs
public partial class MainForm
{
private enum ProcessProperties
{
Id,
Name,
InstancesRunning,
Status,
Owner,
TotalRuntime,
ActiveRuntime,
StartTime,
MemoryUsage,
ProcessPath,
BackgroundProcess,
}
private enum OrderBy
{
Ascending,
Descending,
}
private enum ShowOptions
{
Default,
Active,
Foreground,
}
private static readonly HashSet<Process> blackList = new HashSet<Process>();
private static readonly Dictionary<int, Process> processesInfo = new Dictionary<int, Process>();
private static List<KeyValuePair<int, Process>> filteredProcesses;
private static Dictionary<string, Action> sortingActions;
private static Dictionary<string, Action> orderingActions;
private static Dictionary<string, Action> showOptions;
private static readonly Dictionary<int, ProcessInfo> processesActiveTime = new Dictionary<int, ProcessInfo>();
private static Dictionary<string, Process> autoCompleteCollection;
private static readonly IEnumerable<ProcessProperties> processPropertiesValues =
Enum.GetValues(typeof(ProcessProperties)).Cast<ProcessProperties>();
private static readonly Func<Process, int> GetMemoryUsageInMB = p => (int) (p.WorkingSet64 / (1024 * 1024));
private static readonly Func<Process, TimeSpan> GetRuntimeOfProcess = p => DateTime.Now - p.StartTime;
private static readonly Func<Process, TimeSpan> GetActiveTimeOfProcess =
p => processesActiveTime[p.Id].TimeActive;
private static readonly Func<TimeSpan, string> FormatTimeSpanToString =
t => $"{(int) t.TotalHours} h : {t.Minutes} min";
private static readonly Func<Process, string> GetStatusOfProcess =
p => p.Responding ? "Active" : "Not responding";
private static readonly Func<Func<Process, int>, int> GetAverageLengthOf =
predicate =>
filteredProcesses.Where(process => !process.Value.HasExited)
.GetAverageLengthOf(p => predicate.Invoke(p.Value));
private static readonly Func<Process, string> GetPathOfProcess = p => ProcessInspector.GetProcessPath(p);
private static readonly Func<Process, bool> IsBackgroundProcess = p => ProcessInspector.IsBackgroundProcess(p);
private static readonly Func<Process, int> GetInstaceCountOfProcess =
p => Process.GetProcessesByName(p.ProcessName).Length;
private static readonly Timer updateTimer = new Timer();
private static readonly Timer focusTimeTimer = new Timer();
private static ShowOptions showOption = default(ShowOptions);
private static OrderBy currentOrder = default(OrderBy);
private static string lastSortAction = string.Empty;
private static Process selectedProcess;
//it's functor<string> instead of string as selectedProcess is null at start
//and we need actual reference so we have the latest value
private static readonly Dictionary<ProcessProperties, Func<string>> processProperties = new Dictionary
<ProcessProperties, Func<string>>
{
[ProcessProperties.Id] = () => selectedProcess.Id.ToString(),
[ProcessProperties.Name] = () => selectedProcess.ProcessName,
[ProcessProperties.InstancesRunning] = () => GetInstaceCountOfProcess(selectedProcess).ToString(),
[ProcessProperties.Status] = () => GetStatusOfProcess(selectedProcess),
[ProcessProperties.Owner] = () => ProcessInspector.GetProcessOwner(selectedProcess.Id),
[ProcessProperties.TotalRuntime] = () => FormatTimeSpanToString(GetRuntimeOfProcess(selectedProcess)),
[ProcessProperties.ActiveRuntime] =
() => FormatTimeSpanToString(GetActiveTimeOfProcess(selectedProcess)),
[ProcessProperties.StartTime] = () => selectedProcess.StartTime.ToString("g"),
[ProcessProperties.MemoryUsage] = () => GetMemoryUsageInMB(selectedProcess) + " MB",
[ProcessProperties.ProcessPath] = () => GetPathOfProcess(selectedProcess),
[ProcessProperties.BackgroundProcess] = () => IsBackgroundProcess(selectedProcess).ToString(),
};
}
MainForm.cs
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
InitializeSortingActions();
InitializeOrderingActions();
InitializeShowOptions();
LoadProcesses();
IntializeProcessList();
InitializeInspectedProcessList();
UpdateProcessList();
updateTimer.Interval = 1000 * 10;
updateTimer.Tick += UpdateTimer_Tick;
focusTimeTimer.Interval = 1000;
focusTimeTimer.Tick += FocusTimeTimer_Tick;
focusTimeTimer.Start();
}
private void IntializeProcessList()
{
lvProcesses.Columns.Add("Name".ExtendWithEmptySpaces(GetAverageLengthOf(p => p.ProcessName.Length)));
lvProcesses.Columns.Add("Status".ExtendWithEmptySpaces(GetAverageLengthOf(p => GetStatusOfProcess(p).Length)));
lvProcesses.Columns.Add("Total Runtime".ExtendWithEmptySpaces(GetAverageLengthOf(p => FormatTimeSpanToString(GetRuntimeOfProcess(p)).Length)));
lvProcesses.Columns.Add("Active Runtime".ExtendWithEmptySpaces(GetAverageLengthOf(p => FormatTimeSpanToString(GetActiveTimeOfProcess(p)).Length)));
lvProcesses.Columns.Add("Start Time".ExtendWithEmptySpaces(GetAverageLengthOf(p => p.StartTime.ToString().Length) + 6));
lvProcesses.Columns.Add("Memory Usage".ExtendWithEmptySpaces(GetAverageLengthOf(p => GetMemoryUsageInMB(p).ToString().Length)));
lvProcesses.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
lvProcesses.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);
}
private void InitializeInspectedProcessList()
{
UpdateSearchOptions();
lvInspectedProcess.Columns.Add("Property ");
lvInspectedProcess.Columns.Add("Value ");
lvInspectedProcess.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
lvInspectedProcess.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);
lvInspectedProcess.Items.AddRange(
processPropertiesValues.Select(
value => CreateListViewRow(value.ToString().SplitOnCapitalLetters(), string.Empty)).ToArray());
}
private static Dictionary<string, Process> GetDistinctProcesses()
{
var distinctProcesses = new Dictionary<string, Process>();
foreach (var filteredProcess in filteredProcesses)
{
if (!distinctProcesses.ContainsKey(filteredProcess.Value.ProcessName))
{
distinctProcesses.Add(filteredProcess.Value.ProcessName, filteredProcess.Value);
}
}
return distinctProcesses;
}
private static IEnumerable<string> ExtractSearchOptions()
=> autoCompleteCollection.Values.Select(p => p.ProcessName);
private void UpdateSearchOptions()
{
autoCompleteCollection = GetDistinctProcesses();
tbSearchField.AutoCompleteCustomSource.AddRange(ExtractSearchOptions().ToArray());
}
private static void LoadProcesses()
{
processesActiveTime.RemoveAll((i, info) => info.Process.HasExited);
processesInfo.RemoveAll((i, process) => process.HasExited);
processesActiveTime.RemoveAll((i, info) => info.Process.HasExited);
Process[] allProcesses = Process.GetProcesses();
foreach (var process in allProcesses)
{
try
{
if (blackList.Contains(process) || processesInfo.ContainsKey(process.Id))
{
continue;
}
//attempts to cause deny acess by the process.
if (!process.HasExited)
{
DateTime runtime = process.StartTime;
}
processesInfo.Add(process.Id, process);
processesActiveTime.Add(process.Id, new ProcessInfo(process, new TimeSpan()));
}
catch (Win32Exception) { blackList.Add(process); }
catch (InvalidOperationException) { }
}
showOptions[showOption.ToString()].Invoke();
}
private void InitializeSortingActions()
{
sortingActions = new Dictionary<string, Action>
{
["Name"] = () => SortProcesses(p => p.ProcessName),
["Status"] = () => SortProcesses(p => p.Responding),
["Start Time"] = () => SortProcesses(p => p.StartTime),
["Total Runtime"] = () => SortProcesses(p => GetRuntimeOfProcess(p)),
["Memory Usage"] = () => SortProcesses(p => GetMemoryUsageInMB(p)),
["Active Time"] = () => SortProcesses(p => GetActiveTimeOfProcess(p))
};
foreach (var sortingAction in sortingActions)
{
cbSorting.Items.Add(sortingAction.Key);
}
}
private void InitializeOrderingActions()
{
orderingActions = new Dictionary<string, Action>
{
[OrderBy.Ascending.ToString()] = () => currentOrder = OrderBy.Ascending,
[OrderBy.Descending.ToString()] = () => currentOrder = OrderBy.Descending,
};
foreach (var orderingAction in orderingActions)
{
cbOrders.Items.Add(orderingAction.Key);
}
}
private void InitializeShowOptions()
{
showOptions = new Dictionary<string, Action>
{
[ShowOptions.Default.ToString()] = () =>
{
showOption = ShowOptions.Default;
filteredProcesses = processesInfo.ToList();
},
[ShowOptions.Active.ToString()] = () =>
{
showOption = ShowOptions.Active;
filteredProcesses =
processesInfo.Where(p => !p.Value.HasExited && p.Value.Responding).ToList();
},
[ShowOptions.Foreground.ToString()] = () =>
{
showOption = ShowOptions.Foreground;
filteredProcesses =
processesInfo.Where(p => !p.Value.HasExited && !IsBackgroundProcess(p.Value)).ToList();
},
};
foreach (var option in showOptions)
{
cbShowOptions.Items.Add(option.Key);
}
}
private void SortProcesses<T>(Func<Process, T> lambda)
where T : IComparable
{
filteredProcesses.RemoveAll(p => p.Value.HasExited);
switch (currentOrder)
{
case OrderBy.Descending:
filteredProcesses.Sort(
(process1, process2) => lambda.Invoke(process2.Value).CompareTo(lambda.Invoke(process1.Value)));
break;
case OrderBy.Ascending:
filteredProcesses.Sort(
(process1, process2) => lambda.Invoke(process1.Value).CompareTo(lambda.Invoke(process2.Value)));
break;
}
UpdateProcessList();
}
private void UpdateProcessList()
{
updateTimer.Stop();
updateTimer.Start();
lvProcesses.Items.Clear();
var rows = new List<ListViewItem>(filteredProcesses.Count);
foreach (var processInfo in filteredProcesses)
{
try
{
TimeSpan runtime = GetRuntimeOfProcess(processInfo.Value);
TimeSpan activeTime = GetActiveTimeOfProcess(processInfo.Value);
rows.Add(
CreateListViewRow(
processInfo.Value.ProcessName,
GetStatusOfProcess(processInfo.Value),
FormatTimeSpanToString(runtime),
FormatTimeSpanToString(activeTime),
processInfo.Value.StartTime.ToString("g"),
GetMemoryUsageInMB(processInfo.Value) + " MB"));
}
catch (InvalidOperationException) { }
}
lvProcesses.BeginUpdate();
lvProcesses.Items.AddRange(rows.ToArray());
lvProcesses.EndUpdate();
}
private void RefreshList()
{
LoadProcesses();
UpdateSearchOptions();
if (!string.IsNullOrEmpty(lastSortAction))
{
sortingActions[lastSortAction].Invoke();
}
else
{
UpdateProcessList();
}
}
private static ListViewItem CreateListViewRow(string name, string status, string runtime, string activeTime,
string startTime, string memoryUsage)
{
return new ListViewItem(new[] {name, status, runtime, activeTime, startTime, memoryUsage});
}
private static ListViewItem CreateListViewRow(string propertyName, string value)
{
ListViewItem item = new ListViewItem(propertyName, 0);
item.SubItems.Add(value);
return item;
}
private void EditInspectedProcessSubItem(ProcessProperties processProperty, string value)
{
ListViewItem row = lvInspectedProcess.Items[(int)processProperty];
row.SubItems[1] = new ListViewItem.ListViewSubItem(row, value);
}
#region Event handlers
private void FocusTimeTimer_Tick(object sender, EventArgs e)
{
tbProcessCount.Text = filteredProcesses.Count.ToString();
int activeProcessId = ProcessInspector.GetActiveProcessId();
if (activeProcessId == -1)
{
return;
}
ProcessInfo activeProcess;
if (processesActiveTime.TryGetValue(activeProcessId, out activeProcess))
{
try
{
activeProcess.TimeActive =
activeProcess.TimeActive.Add(new TimeSpan(0, 0, focusTimeTimer.Interval / 1000));
if (activeProcess.TimeActive.Seconds == 0 && activeProcess.TimeActive.Minutes == 0 &&
activeProcess.TimeActive.TotalHours > 0)
{
MessageBox.Show(
$@"You've spent {activeProcess.TimeActive.TotalHours} hours on '{activeProcess.Process
.ProcessName}'");
}
}
catch (InvalidOperationException)
{
processesActiveTime.Remove(activeProcessId);
}
}
else
{
RefreshList();
}
}
private void UpdateTimer_Tick(object sender, EventArgs e)
{
if (cbEnableAutoUpdate.Checked)
{
RefreshList();
}
}
private void bUpdate_Click(object sender, EventArgs e)
{
RefreshList();
}
private void ComboBoxSorting_SelectedIndexChanged(object sender, EventArgs e)
{
string itemText = ((Control)sender).Text;
if (!string.IsNullOrEmpty(itemText))
{
lastSortAction = ((Control) sender).Text;
sortingActions[lastSortAction].Invoke();
}
}
private void ComboBoxOrders_SelectedIndexChanged(object sender, EventArgs e)
{
string itemText = ((Control)sender).Text;
if (!string.IsNullOrEmpty(itemText) && itemText != showOption.ToString())
{
orderingActions[itemText].Invoke();
if (!string.IsNullOrEmpty(lastSortAction))
{
sortingActions[lastSortAction].Invoke();
}
}
}
private void ComboBoxShowOptions_SelectedIndexChanged(object sender, EventArgs e)
{
string itemText = ((Control) sender).Text;
if (!string.IsNullOrEmpty(itemText) && itemText != showOption.ToString())
{
showOptions[itemText].Invoke();
if (!string.IsNullOrEmpty(lastSortAction))
{
sortingActions[lastSortAction].Invoke();
}
else
{
UpdateProcessList();
}
}
}
private void ProcessesListView_SelectedIndexChanged(object sender, EventArgs e)
{
if (lvProcesses.SelectedIndices.Count == 0)
{
return;
}
int processIndex = lvProcesses.SelectedIndices[0]; //only 1 item can be selected anyway
selectedProcess = filteredProcesses[processIndex].Value;
if (selectedProcess.HasExited)
{
RefreshList();
selectedProcess = null;
}
else
{
tbSearchField.Text = selectedProcess.ProcessName;
}
}
private void MainForm_Resize(object sender, EventArgs e)
{
if (WindowState == FormWindowState.Minimized && !ProcessTrackerIcon.Visible)
{
ProcessTrackerIcon.Visible = true;
ProcessTrackerIcon.BalloonTipText = @"Minimized to tray";
ProcessTrackerIcon.ShowBalloonTip(1000);
ShowInTaskbar = false;
}
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
ProcessTrackerIcon.Visible = false;
base.OnFormClosing(e);
}
private void ProcessTrackerIcon_MouseDoubleClick(object sender, MouseEventArgs e)
{
WindowState = FormWindowState.Normal;
ShowInTaskbar = true;
ProcessTrackerIcon.Visible = false;
}
private void ProcessTrackerIcon_MouseClick(object sender, MouseEventArgs e)
{
WindowState = FormWindowState.Normal;
ShowInTaskbar = true;
ProcessTrackerIcon.Visible = false;
}
private void bInspect_Click(object sender, EventArgs e)
{
if (selectedProcess == null)
{
MessageBox.Show(@"No process selected !");
return;
}
if (selectedProcess.HasExited)
{
MessageBox.Show(@"Selected process has already been terminated !");
RefreshList();
return;
}
for (int i = 0; i < lvInspectedProcess.Items.Count; i++)
{
ProcessProperties processProperty = (ProcessProperties) i;
EditInspectedProcessSubItem(processProperty, processProperties[processProperty].Invoke());
}
}
private void bTerminate_Click(object sender, EventArgs e)
{
ApplyActionOnSelectedProcess(() =>
{
selectedProcess.Kill();
},
$"{selectedProcess.ProcessName} was successfully terminated",
$"Failed terminating {selectedProcess.ProcessName}");
selectedProcess = null;
RefreshList();
}
private void bRefresh_Click(object sender, EventArgs e)
{
ApplyActionOnSelectedProcess(() => selectedProcess.Refresh(),
$"{selectedProcess.ProcessName} was successfully refreshed",
$"Failed refreshing {selectedProcess.ProcessName}");
}
private static void ApplyActionOnSelectedProcess(Action action, string successMessage, string failedMessage)
{
if (selectedProcess == null)
{
MessageBox.Show(@"No process selected");
return;
}
try
{
action();
MessageBox.Show(successMessage);
}
catch (Exception)
{
MessageBox.Show(failedMessage);
}
}
private void SearchFieldTextBox_TextChanged(object sender, EventArgs e)
{
autoCompleteCollection.TryGetValue(tbSearchField.Text, out selectedProcess);
}
#endregion
}
Thank you for taking your time to read that.