I\'m trying to create a Popularity Contest for Forms in our primary front end. There are many items that are no longer used, but getting details on which are used and which
Yeah, this should be easy. There are event hooks like OnLoad, OnShow, OnClose() for all forms and most user controls. If you wanted to see, at a more granule level what controls are being used by your users, you can hook up OnClick(), OnMouseOver() and about a hundred other events.
... and you can create your own custom events.
So, hook up the events by selecting the form, then properties (right click or F4 key). In the properties window at the top, you've got a "show events" button that looks like a lightning bolt. Click that and then pick, from the list, the event you want to use for this logging.
A not so expensive (maybe) solution can be this:
Create a new class MyBaseForm
, which inherits from System.Windows.Forms.Form
, and handle its load event in the way you need.
Now the hard part: modify all of the existing forms classes so they inherit from MyBaseForm
and not from the default System.Windows.Forms.Form
; and be sure you do the same for every future Form you will add to your solution.
Not bullet proof at all, it can be easy to forget to modify the base class for a new form and/or to miss the modification for an existing form class
But you can give it a try
Applying an IMessageFilter to the application to detect the WM_Create message and then determining if the target handle belonged to a Form
would be ideal solution with a minimal performance hit. Unfortunately, that message does not get passed to the filter. As an alternative, I have selected the WM_Paint message to reduce the performance impact. The following filter code creates a dictionary of form type names and a count of Form's with that name ultimate disposal. The Form.Closed Event is not reliable under all closure conditions, but the Disposed event appears reliable.
internal class FormCreationFilter : IMessageFilter
{
private List<Form> trackedForms = new List<Form>();
internal Dictionary<string, Int32> formCounter = new Dictionary<string, Int32>(); // FormName, CloseCount
public bool PreFilterMessage(ref Message m)
{
// Ideally we would trap the WM_Create, butthe message is not routed through
// the message filter mechanism. It is sent directly to the window.
// Therefore use WM_Paint as a surrgogate filter to prevent the lookup logic
// from running on each message.
const Int32 WM_Paint = 0xF;
if (m.Msg == WM_Paint)
{
Form f = Control.FromChildHandle(m.HWnd) as Form;
if (f != null && !(trackedForms.Contains(f)))
{
trackedForms.Add(f);
f.Disposed += IncrementFormDisposed;
}
}
return false;
}
private void IncrementFormDisposed(object sender, EventArgs e)
{
Form f = sender as Form;
if (f != null)
{
string name = f.GetType().Name;
if (formCounter.ContainsKey(name))
{
formCounter[name] += 1;
}
else
{
formCounter[name] = 1;
}
f.Disposed -= IncrementFormDisposed;
trackedForms.Remove(f);
}
}
}
Create an instance and install the filter similar to the following example. The foreach
loop is just shown to demonstrate accessing the count.
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
FormCreationFilter mf = new FormCreationFilter();
Application.AddMessageFilter(mf);
Application.Run(new Form1());
Application.RemoveMessageFilter(mf);
foreach (KeyValuePair<string, Int32> kvp in mf.formCounter)
{
Debug.Print($"{kvp.Key} opened {kvp.Value} times. ");
}
}
I'm posting the code that is required to detect and log Forms activity, for testing or for comparison reasons.
As shown, this code only needs to be inserted in the Program.cs
file, inside the Main method.
This procedure logs each new opened Form's Title/Caption and the Form's Name.
Other elements can be added to the log, possibly using a dedicated method.
When a new WindowPattern.WindowOpenedEvent event detects that a new Window is created, the AutomationElement.ProcessId
is compared with the Application's ProcessId to determine whether the new Window belongs to the Application.
The Application.OpenForms()
collection is then parsed, using the Form.AccessibleObject cast to Control.ControlAccessibleObject to compare the AutomationElelement.NativeWindowHandle
with a Form.Handle
property, to avoid Invoking the UI Thread to get the handle of a Form (which can generate exceptions or thread locks, since the Forms are just loading at that time).
using System.Diagnostics;
using System.IO;
using System.Security.Permissions;
using System.Windows.Automation;
static class Program
{
[STAThread]
[SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.ControlAppDomain)]
static void Main(string[] args)
{
Automation.AddAutomationEventHandler(
WindowPattern.WindowOpenedEvent, AutomationElement.RootElement,
TreeScope.Subtree, (uiElm, evt) => {
AutomationElement element = uiElm as AutomationElement;
if (element == null) return;
try
{
if (element.Current.ProcessId == Process.GetCurrentProcess().Id)
{
IntPtr elmHandle = (IntPtr)element.Current.NativeWindowHandle;
Control form = Application.OpenForms.OfType<Control>()
.FirstOrDefault(f => (f.AccessibilityObject as Control.ControlAccessibleObject).Handle == elmHandle);
string log = $"Name: {form?.Name ?? element.Current.AutomationId} " +
$"Form title: {element.Current.Name}{Environment.NewLine}";
File.AppendAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "formLogger.txt"), log);
}
}
catch (ElementNotAvailableException) { /* May happen when Debugging => ignore or log */ }
});
}
}